Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

redone enable-abuseipdb reporting #114

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
endlessh-go
endlessh-go.exe
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,14 @@ Also check out [examples](./examples/README.md) for the setup of the full stack.

```
Usage of ./endlessh-go
-abuse_ipdb_api_key string
AbuseIPDB API key
-alsologtostderr
log to standard error as well as files
-conn_type string
Connection type. Possible values are tcp, tcp4, tcp6 (default "tcp")
-enable_abuseipdb
Enable AbuseIPDB reporting
-enable_prometheus
Enable prometheus
-geoip_supplier string
Expand Down Expand Up @@ -104,12 +108,16 @@ Endlessh-go exports the following Prometheus metrics.

The metrics is off by default, you can turn it via the CLI argument `-enable_prometheus`.

AbuseIPDB reporting is also off by default, you can turn it on via the CLI argument '-enable_abuseipdb'

It listens to port `2112` and entry point is `/metrics` by default. The port and entry point can be changed via CLI arguments.

The endlessh-go server stores the geohash of attackers as a label on `endlessh_client_open_count`, which is also off by default. You can turn it on via the CLI argument `-geoip_supplier`. The endlessh-go uses service from [ip-api](https://ip-api.com/), which may enforce a query rate and limit commercial use. Visit their website for their terms and policies.

You could also use an offline GeoIP database from [MaxMind](https://www.maxmind.com) by setting `-geoip_supplier` to _max-mind-db_ and `-max_mind_db` to the path of the database file.

The AbuseIPDB reporting requires their free to use API available at their [website](https://www.abuseipdb.com/pricing), once you have it, add it as a docker environment variable `ABUSE_IPDB_API_KEY`

## Dashboard

The dashboard requires Grafana 8.2.
Expand Down
26 changes: 26 additions & 0 deletions examples/docker-abuseipdb/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
version: '3.5'
services:
endlessh:
container_name: endlessh
image: shizunge/endlessh-go:latest
restart: unless-stopped
command:
- "-logtostderr"
- "-v=1"
- "-enable_abuseipdb"
networks:
- example_network
ports:
- "2222:2222" # SSH port
- "127.0.0.1:2112:2112" # Prometheus metrics port
secrets:
- abuseipdb_api_key
environment:
ABUSE_IPDB_API_KEY_FILE: /run/secrets/abuseipdb_api_key

networks:
example_network:

secrets:
abuseipdb_api_key:
external: true # Ensure the secret is created beforehand with 'docker secret create'
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.21.0
toolchain go1.21.4

require (
github.com/dgraph-io/ristretto v0.1.1
github.com/golang/glog v1.2.0
github.com/oschwald/geoip2-golang v1.9.0
github.com/pierrre/geohash v1.1.1
Expand All @@ -14,8 +15,10 @@ require (
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/oschwald/maxminddb-golang v1.11.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.45.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
Expand Down
15 changes: 15 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,20 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/broady/gogeohash v0.0.0-20120525094510-7b2c40d64042 h1:iEdmkrNMLXbM7ecffOAtZJQOQUTE4iMonxrb5opUgE4=
github.com/broady/gogeohash v0.0.0-20120525094510-7b2c40d64042/go.mod h1:f1L9YvXvlt9JTa+A17trQjSMM6bV40f+tHjB+Pi+Fqk=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/fanixk/geohash v0.0.0-20150324002647-c1f9b5fa157a h1:Fyfh/dsHFrC6nkX7H7+nFdTd1wROlX/FxEIWVpKYf1U=
github.com/fanixk/geohash v0.0.0-20150324002647-c1f9b5fa157a/go.mod h1:UgNw+PTmmGN8rV7RvjvnBMsoTU8ZXXnaT3hYsDTBlgQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68=
github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
Expand All @@ -36,6 +44,8 @@ github.com/pierrre/go-libs v0.2.14 h1:wAPoOrslKLnha6ow5EKkxxZpo76kOea57efs71A/Zn
github.com/pierrre/go-libs v0.2.14/go.mod h1:eA3pQD5LHZmavOpTpUfO8FszduBNHoFXDWrevDR6Dy8=
github.com/pierrre/pretty v0.0.10 h1:Cb5som+1EpU+x7UA5AMy9I8AY2XkzMBywkLEAdo1JDg=
github.com/pierrre/pretty v0.0.10/go.mod h1:F+Z4XV4T5GIvbr/swCAkuQ2ng81qMaQT9CfI8rKOLdY=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
Expand All @@ -46,15 +56,20 @@ github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lne
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/the42/cartconvert v1.0.0 h1:g8kt6ic2GEhdcZ61ZP9GsWwhosVo5nCnH1n2/oAQXUU=
github.com/the42/cartconvert v1.0.0/go.mod h1:fWO/msnJVhHqN1yX6OBoxSyfj7TEj1hHiL8bJSQsK30=
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
132 changes: 130 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,14 @@ import (
"endlessh-go/metrics"
"flag"
"fmt"
"io"
"net"
"net/http"
"os"
"strings"
"time"

"github.com/dgraph-io/ristretto"
"github.com/golang/glog"
)

Expand Down Expand Up @@ -67,7 +70,7 @@ func startSending(maxClients int64, bannerMaxLength int64, records chan<- metric
return clients
}

func startAccepting(maxClients int64, connType, connHost, connPort string, interval time.Duration, clients chan<- *client.Client, records chan<- metrics.RecordEntry) {
func startAccepting(maxClients int64, connType, connHost, connPort string, interval time.Duration, clients chan<- *client.Client, records chan<- metrics.RecordEntry, abuseipdbeEnabled bool, abuseIpdbApiKey string) {
go func() {
l, err := net.Listen(connType, connHost+":"+connPort)
if err != nil {
Expand All @@ -92,10 +95,113 @@ func startAccepting(maxClients int64, connType, connHost, connPort string, inter
LocalPort: connPort,
}
clients <- c
go reportIPToAbuseIPDB(remoteIpAddr, records, abuseipdbeEnabled, abuseIpdbApiKey)
}
}()
}

func reportIPToAbuseIPDB(ip string, records chan<- metrics.RecordEntry, abuseipdbeEnabled bool, abuseIpdbApiKey string) {
if !abuseipdbeEnabled {
return
}
if isCached(ip) {
glog.V(1).Infof("IP is already cached, skipping report")
records <- metrics.RecordEntry{
RecordType: metrics.RecordEntryTypeReport,
IpAddr: ip,
Message: "IP is already cached, skipping report",
}
return
}
appendToReportedIPs(ip) // Cache the IP before possibly early exiting due to API key issues

var apiKey string
if abuseIpdbApiKey != "" {
apiKey = abuseIpdbApiKey
} else {
glog.V(1).Infof("AbuseIPDB API key not set, skipping report")
records <- metrics.RecordEntry{
RecordType: metrics.RecordEntryTypeReport,
IpAddr: ip,
Message: "AbuseIPDB API key not set, skipping report",
}
return
}

// Format the timestamp in ISO 8601 format, including the timezone (Z for UTC)
timestamp := time.Now().UTC().Format(time.RFC3339)
reportURL := fmt.Sprintf("https://api.abuseipdb.com/api/v2/report?ip=%s&categories=18,22&comment=SSH honeypot connection attempt.&timestamp=%s", ip, timestamp)
req, err := http.NewRequest("POST", reportURL, nil)
if err != nil {
glog.V(1).Infof("Error creating request: %v", err)
records <- metrics.RecordEntry{
RecordType: metrics.RecordEntryTypeReport,
IpAddr: ip,
Message: fmt.Sprintf("Error creating request: %v", err),
}
return
}

req.Header.Set("Key", apiKey)
req.Header.Set("Accept", "application/json")

resp, err := http.DefaultClient.Do(req)
if err != nil {
records <- metrics.RecordEntry{
RecordType: metrics.RecordEntryTypeReport,
IpAddr: ip,
Message: fmt.Sprintf("Error making request: %v", err),
}
return
}
defer resp.Body.Close()

if resp.StatusCode == http.StatusUnprocessableEntity {
body, err := io.ReadAll(resp.Body)
if err != nil {
glog.V(1).Infof("Error reading response body: %v", err)
} else {
glog.V(1).Infof("Unprocessable Entity response from AbuseIPDB: %s", body)
}
records <- metrics.RecordEntry{
RecordType: metrics.RecordEntryTypeReport,
IpAddr: ip,
Message: fmt.Sprintf("Unprocessable Entity response from AbuseIPDB: %s", resp.Status),
}
return
}

glog.V(1).Infof("Reported IP to AbuseIPDB: %s", resp.Status)
records <- metrics.RecordEntry{
RecordType: metrics.RecordEntryTypeReport,
IpAddr: ip,
Message: fmt.Sprintf("Reported IP to AbuseIPDB: %s", resp.Status),
}
}

func setupCache() {
config := &ristretto.Config{
NumCounters: 1e7, // NumCounters is 10x the number of items you expect to keep in the cache when full.
MaxCost: 1 << 30, // Maximum cost of cache (e.g., bytes if the cost is measured in bytes).
BufferItems: 64, // Number of keys per Get buffer.
}

var err error
cache, err = ristretto.NewCache(config)
if err != nil {
glog.Fatalf("Failed to create cache: %v", err)
}
}

func isCached(ip string) bool {
_, found := cache.Get(ip)
return found
}

func appendToReportedIPs(ip string) {
cache.Set(ip, struct{}{}, 1)
}

type arrayStrings []string

func (a *arrayStrings) String() string {
Expand All @@ -109,15 +215,20 @@ func (a *arrayStrings) Set(value string) error {

const defaultPort = "2222"

var cache *ristretto.Cache
var connPorts arrayStrings

func main() {
setupCache()

abuseIPDBApiKeyFlag := flag.String("abuse_ipdb_api_key", "", "AbuseIPDB API key")
intervalMs := flag.Int("interval_ms", 1000, "Message millisecond delay")
bannerMaxLength := flag.Int64("line_length", 32, "Maximum banner line length")
maxClients := flag.Int64("max_clients", 4096, "Maximum number of clients")
connType := flag.String("conn_type", "tcp", "Connection type. Possible values are tcp, tcp4, tcp6")
connHost := flag.String("host", "0.0.0.0", "SSH listening address")
flag.Var(&connPorts, "port", fmt.Sprintf("SSH listening port. You may provide multiple -port flags to listen to multiple ports. (default %q)", defaultPort))
abuseipdbeEnabled := flag.Bool("enable_abuseipdb", false, "Enable AbuseIPDB reporting")
prometheusEnabled := flag.Bool("enable_prometheus", false, "Enable prometheus")
prometheusHost := flag.String("prometheus_host", "0.0.0.0", "The address for prometheus")
prometheusPort := flag.String("prometheus_port", "2112", "The port for prometheus")
Expand All @@ -132,6 +243,23 @@ func main() {
}
flag.Parse()

var apiKey string
if *abuseIPDBApiKeyFlag != "" {
apiKey = *abuseIPDBApiKeyFlag
} else {
abuseIPDBApiKeyFile := os.Getenv("ABUSE_IPDB_API_KEY_FILE")
if abuseIPDBApiKeyFile != "" {
key, err := os.ReadFile(abuseIPDBApiKeyFile)
if err != nil {
glog.Warningf("Error reading API key file: %v", err)
} else {
apiKey = strings.TrimSpace(string(key))
}
} else {
glog.Warning("Neither abuse_ipdb_api_key flag nor ABUSE_IPDB_API_KEY_FILE environment variable is set. AbuseIPDB reporting will be disabled.")
}
}

if *prometheusEnabled {
if *connType == "tcp6" && *prometheusHost == "0.0.0.0" {
*prometheusHost = "[::]"
Expand All @@ -155,7 +283,7 @@ func main() {
connPorts = append(connPorts, defaultPort)
}
for _, connPort := range connPorts {
startAccepting(*maxClients, *connType, *connHost, connPort, interval, clients, records)
startAccepting(*maxClients, *connType, *connHost, connPort, interval, clients, records, *abuseipdbeEnabled, apiKey)
}
for {
if *prometheusCleanUnseenSeconds <= 0 {
Expand Down
10 changes: 6 additions & 4 deletions metrics/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,11 @@ func InitPrometheus(prometheusHost, prometheusPort, prometheusEntry string) {
}

const (
RecordEntryTypeStart = iota
RecordEntryTypeSend = iota
RecordEntryTypeStop = iota
RecordEntryTypeClean = iota
RecordEntryTypeStart = iota
RecordEntryTypeSend = iota
RecordEntryTypeStop = iota
RecordEntryTypeClean = iota
RecordEntryTypeReport = iota
)

type RecordEntry struct {
Expand All @@ -108,6 +109,7 @@ type RecordEntry struct {
LocalPort string
MillisecondsSpent int64
BytesSent int
Message string
}

func StartRecording(maxClients int64, prometheusEnabled bool, prometheusCleanUnseenSeconds int, geoOption geoip.GeoOption) chan RecordEntry {
Expand Down