26 Commits
Author SHA1 Message Date
kor-elf e44264c21c Add commented examples for excluding IPs/subnets in blocklist configurations across use cases 2026-08-12 16:29:30 +05:00
kor-elf 407c9d961f Refactor IP parsing to use appendIfNotExcluded for exclusion validation and error handling 2026-08-12 16:29:09 +05:00
kor-elf c22ef390ee Add NewConfigWithExclusionChecker to support exclusion checker in config creation 2026-08-12 16:28:12 +05:00
kor-elf 6f395d1a3b Add IsExcluded method and ExclusionChecker support to IP validators 2026-08-12 16:27:09 +05:00
kor-elf 797dac73a0 Add ExclusionChecker implementation for validating and filtering IP ranges by exclusion rules 2026-08-12 16:14:03 +05:00
kor-elf a8364f98ed Add examples for fetching and displaying separated IPv4 and IPv6 addresses from blocklist services 2026-03-18 21:05:30 +05:00
kor-elf a2dff48936 Refactor request logic with reusable fetch function and add methods to handle separated IP parsing for both regular and ZIP responses. 2026-03-18 21:05:11 +05:00
kor-elf e09411f871 Add method to parse IPs by version (IPv4/IPv6) with validation and limit support 2026-03-18 21:04:33 +05:00
kor-elf 1b16ae243f Add support for parsing IPs by version (IPv4 and IPv6) with validation logic 2026-03-18 21:03:50 +05:00
kor-elf cfbf4ce504 Add StopForumSpam example link to README 2026-03-15 20:28:36 +05:00
kor-elf 17115d97c6 Add StopForumSpam example for fetching and parsing IP blocklist data 2026-03-15 20:27:58 +05:00
kor-elf 1dc7a8b22c Add ZIP archive handling support for fetching and parsing blocklist data 2026-03-15 20:27:31 +05:00
kor-elf e4fc07871b Add GreenSnow example link to README 2026-03-15 19:06:26 +05:00
kor-elf faedb345df Add Greensnow example for fetching and parsing IP blocklist data 2026-03-15 19:05:16 +05:00
kor-elf ee0e36b40d Add Blocklist.de example link to README 2026-03-15 18:59:25 +05:00
kor-elf 790073aa95 Add Blocklist.de example for fetching and parsing IP blocklist data 2026-03-15 18:58:19 +05:00
kor-elf e18859e78f Add a link to Daniel Gerzo's BruteforceBlocker list usage example to your README file. 2026-03-15 18:18:05 +05:00
kor-elf aeb855c0b8 Add BruteforceBlocker example for fetching and parsing IP blocklist data 2026-03-15 18:07:40 +05:00
kor-elf 5b8974101e Add CIArmy example link to README 2026-03-15 17:48:00 +05:00
kor-elf dd4e5619c7 Add CIArmy example for fetching and parsing IP blocklist data 2026-03-15 17:47:14 +05:00
kor-elf 172c32237b Normalize capitalization of "HONEYPOT" in README example link. 2026-03-15 17:40:42 +05:00
kor-elf 364e657641 Fix typo in README by removing unnecessary hyphen in "IP адресов" 2026-03-15 16:48:58 +05:00
kor-elf e4b0ed6668 Clarify README description by generalizing "bad IP addresses" to "IP addresses" 2026-03-15 16:47:49 +05:00
kor-elf f295c402af Add README with installation steps, usage examples, and license information 2026-03-15 16:45:31 +05:00
kor-elf 062cb25551 Add Tor example for fetching and parsing IP blocklist data 2026-03-15 16:43:14 +05:00
kor-elf 17dbfac864 Clarify example comments by generalizing "bad IP addresses" to "IP addresses" 2026-03-15 16:41:28 +05:00
17 changed files with 1359 additions and 27 deletions
+23
View File
@@ -0,0 +1,23 @@
# blocklist
Пакет Go для получения IP адресов от различных сервисов.
## Установка
```sh
go get git.kor-elf.net/kor-elf-shield/blocklist
```
## Примеры использования
- [Пример получения списка IP адресов от Spamhaus](/examples/spamhaus.go)
- [Пример получения списка IP адресов от DShield](/examples/dshield.go)
- [Пример получения списка IP адресов от HONEYPOT](/examples/honeypot.go)
- [Пример получения списка IP адресов от Tor](/examples/tor.go)
- [Пример получения списка IP адресов от CIARMY](/examples/ciarmy.go)
- [Пример получения списка IP адресов от Daniel Gerzo (BruteforceBlocker)](/examples/bruteforceblocker.go)
- [Пример получения списка IP адресов от Blocklist.de](/examples/blocklist.go)
- [Пример получения списка IP адресов от GreenSnow](/examples/greensnow.go)
- [Пример получения списка IP адресов от StopForumSpam](/examples/stopforumspam.go)
## Лицензия
[MIT](https://git.kor-elf.net/kor-elf-shield/blocklist/src/branch/main/LICENSE)
+268 -19
View File
@@ -1,10 +1,14 @@
package blocklist
import (
"archive/zip"
"bytes"
"context"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"git.kor-elf.net/kor-elf-shield/blocklist/parser"
@@ -16,6 +20,12 @@ const (
// requestTimeout defines the maximum duration for request operations before timing out.
requestTimeout = 20 * time.Second
// maxDownloadSize defines the maximum allowed size of the downloaded file in bytes.
maxDownloadSize int64 = 20 << 20 // 20 MiB
// maxArchiveFileSize defines the maximum allowed size of the extracted file from ZIP in bytes.
maxArchiveFileSize uint64 = 50 << 20 // 50 MiB
)
// Config defines the configuration for the blocklist.
@@ -34,6 +44,17 @@ type Config struct {
RequestTimeout time.Duration
}
type ConfigZip struct {
// Config is the configuration for the blocklist.
Config Config
// MaxDownloadSize defines the maximum allowed size of the downloaded file in bytes.
MaxDownloadSize int64
// MaxArchiveFileSize defines the maximum allowed size of the extracted file from ZIP in bytes.
MaxArchiveFileSize uint64
}
// NewConfig creates a new Config with default values.
// limit is the maximum number of items to process or validate. 0 means no limit.
func NewConfig(limit uint) Config {
@@ -45,6 +66,17 @@ func NewConfig(limit uint) Config {
}
}
func NewConfigWithExclusionChecker(limit uint, exclusionChecker parser.ExclusionChecker) Config {
return Config{
Limit: limit,
Validator: &parser.DefaultIPValidator{
ExclusionChecker: exclusionChecker,
},
ContextTimeout: contextTimeout,
RequestTimeout: requestTimeout,
}
}
// NewConfigWithValidator creates a new Config with the specified validator.
// limit is the maximum number of items to process or validate. 0 means no limit.
// validator is the IP validator to use.
@@ -57,32 +89,23 @@ func NewConfigWithValidator(limit uint, validator parser.IPValidator) Config {
}
}
func NewConfigZip(c Config) ConfigZip {
return ConfigZip{
Config: c,
MaxDownloadSize: maxDownloadSize,
MaxArchiveFileSize: maxArchiveFileSize,
}
}
// Get fetches data from the given URL, parses the response using the provided parser, and applies the given configuration.
// It returns the parsed IPs and any errors that occurred during the process.
func Get(fileUrl string, parser parser.Parser, c Config) (parser.IPs, error) {
parsedURL, err := url.Parse(fileUrl)
if err != nil {
return nil, fmt.Errorf("invalid url: %w", err)
}
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
return nil, fmt.Errorf("invalid url scheme: %s", parsedURL.Scheme)
}
ctx, cancel := context.WithTimeout(context.Background(), c.ContextTimeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fileUrl, nil)
res, err := fetch(fileUrl, ctx, c.RequestTimeout)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
client := &http.Client{
Timeout: c.RequestTimeout,
}
res, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
return nil, err
}
defer func() {
_ = res.Body.Close()
@@ -94,3 +117,229 @@ func Get(fileUrl string, parser parser.Parser, c Config) (parser.IPs, error) {
return parser.Parse(res.Body, c.Validator, c.Limit)
}
// GetSeparatedIPs fetches data from the given URL, parses the response using the provided parser, and applies the given configuration.
// It returns the parsed IPs and any errors that occurred during the process.
func GetSeparatedIPs(fileUrl string, parser parser.Parser, c Config) (ipV4 parser.IPs, ipV6 parser.IPs, err error) {
ctx, cancel := context.WithTimeout(context.Background(), c.ContextTimeout)
defer cancel()
res, err := fetch(fileUrl, ctx, c.RequestTimeout)
if err != nil {
return nil, nil, err
}
defer func() {
_ = res.Body.Close()
}()
if res.StatusCode != http.StatusOK {
return nil, nil, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
return parser.ParseIPsByVersion(res.Body, c.Validator, c.Limit)
}
// GetZip fetches data from the given URL, parses the response using the provided parser, and applies the given configuration.
// It returns the parsed IPs and any errors that occurred during the process.
func GetZip(fileUrl string, parser parser.Parser, c ConfigZip) (parser.IPs, error) {
ctx, cancel := context.WithTimeout(context.Background(), c.Config.ContextTimeout)
defer cancel()
res, err := fetch(fileUrl, ctx, c.Config.RequestTimeout)
if err != nil {
return nil, err
}
defer func() {
_ = res.Body.Close()
}()
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
if c.MaxDownloadSize > 0 && res.ContentLength > c.MaxDownloadSize {
return nil, fmt.Errorf("downloaded file is too large: content-length %d exceeds limit %d", res.ContentLength, c.MaxDownloadSize)
}
reader := res.Body
if c.MaxDownloadSize > 0 {
reader = io.NopCloser(io.LimitReader(res.Body, c.MaxDownloadSize+1))
}
body, err := io.ReadAll(reader)
if err != nil {
return nil, fmt.Errorf("read response body: %w", err)
}
if c.MaxDownloadSize > 0 && int64(len(body)) > c.MaxDownloadSize {
return nil, fmt.Errorf("downloaded file exceeds limit %d bytes", c.MaxDownloadSize)
}
if !isZip(body) {
return nil, fmt.Errorf("invalid zip archive")
}
return parseZip(body, parser, c)
}
// GetZipSeparatedIPs fetches data from the given URL, parses the response using the provided parser, and applies the given configuration.
// It returns the parsed IPs and any errors that occurred during the process.
func GetZipSeparatedIPs(fileUrl string, parser parser.Parser, c ConfigZip) (ipV4 parser.IPs, ipV6 parser.IPs, err error) {
ctx, cancel := context.WithTimeout(context.Background(), c.Config.ContextTimeout)
defer cancel()
res, err := fetch(fileUrl, ctx, c.Config.RequestTimeout)
if err != nil {
return nil, nil, err
}
defer func() {
_ = res.Body.Close()
}()
if res.StatusCode != http.StatusOK {
return nil, nil, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
if c.MaxDownloadSize > 0 && res.ContentLength > c.MaxDownloadSize {
return nil, nil, fmt.Errorf("downloaded file is too large: content-length %d exceeds limit %d", res.ContentLength, c.MaxDownloadSize)
}
reader := res.Body
if c.MaxDownloadSize > 0 {
reader = io.NopCloser(io.LimitReader(res.Body, c.MaxDownloadSize+1))
}
body, err := io.ReadAll(reader)
if err != nil {
return nil, nil, fmt.Errorf("read response body: %w", err)
}
if c.MaxDownloadSize > 0 && int64(len(body)) > c.MaxDownloadSize {
return nil, nil, fmt.Errorf("downloaded file exceeds limit %d bytes", c.MaxDownloadSize)
}
if !isZip(body) {
return nil, nil, fmt.Errorf("invalid zip archive")
}
return parseZipSeparatedIPs(body, parser, c)
}
func isZip(body []byte) bool {
return len(body) >= 4 &&
body[0] == 'P' &&
body[1] == 'K' &&
body[2] == 0x03 &&
body[3] == 0x04
}
func parseZip(body []byte, p parser.Parser, c ConfigZip) (parser.IPs, error) {
reader, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
if err != nil {
return nil, fmt.Errorf("open zip archive: %w", err)
}
file := findArchiveFile(reader.File)
if file == nil {
return nil, fmt.Errorf("zip archive does not contain a supported file")
}
if c.MaxArchiveFileSize > 0 && file.UncompressedSize64 > c.MaxArchiveFileSize {
return nil, fmt.Errorf("file %q in zip is too large: %d exceeds limit %d", file.Name, file.UncompressedSize64, c.MaxArchiveFileSize)
}
rc, err := file.Open()
if err != nil {
return nil, fmt.Errorf("open file %q from zip: %w", file.Name, err)
}
defer func() {
_ = rc.Close()
}()
var zipReader io.Reader = rc
if c.MaxArchiveFileSize > 0 {
zipReader = io.LimitReader(rc, int64(c.MaxArchiveFileSize)+1)
}
return p.Parse(zipReader, c.Config.Validator, c.Config.Limit)
}
func parseZipSeparatedIPs(body []byte, p parser.Parser, c ConfigZip) (ipV4 parser.IPs, ipV6 parser.IPs, err error) {
reader, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
if err != nil {
return nil, nil, fmt.Errorf("open zip archive: %w", err)
}
file := findArchiveFile(reader.File)
if file == nil {
return nil, nil, fmt.Errorf("zip archive does not contain a supported file")
}
if c.MaxArchiveFileSize > 0 && file.UncompressedSize64 > c.MaxArchiveFileSize {
return nil, nil, fmt.Errorf("file %q in zip is too large: %d exceeds limit %d", file.Name, file.UncompressedSize64, c.MaxArchiveFileSize)
}
rc, err := file.Open()
if err != nil {
return nil, nil, fmt.Errorf("open file %q from zip: %w", file.Name, err)
}
defer func() {
_ = rc.Close()
}()
var zipReader io.Reader = rc
if c.MaxArchiveFileSize > 0 {
zipReader = io.LimitReader(rc, int64(c.MaxArchiveFileSize)+1)
}
return p.ParseIPsByVersion(zipReader, c.Config.Validator, c.Config.Limit)
}
func findArchiveFile(files []*zip.File) *zip.File {
var fallback *zip.File
for _, file := range files {
if file.FileInfo().IsDir() {
continue
}
if fallback == nil {
fallback = file
}
name := strings.ToLower(file.Name)
if strings.HasSuffix(name, ".txt") ||
strings.HasSuffix(name, ".json") ||
strings.HasSuffix(name, ".xml") ||
strings.HasSuffix(name, ".rss") {
return file
}
}
return fallback
}
func fetch(fileUrl string, ctx context.Context, requestTimeout time.Duration) (*http.Response, error) {
parsedURL, err := url.Parse(fileUrl)
if err != nil {
return nil, fmt.Errorf("invalid url: %w", err)
}
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
return nil, fmt.Errorf("invalid url scheme: %s", parsedURL.Scheme)
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fileUrl, nil)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
client := &http.Client{
Timeout: requestTimeout,
}
res, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
return res, nil
}
+98
View File
@@ -0,0 +1,98 @@
package main
import (
"fmt"
"git.kor-elf.net/kor-elf-shield/blocklist"
"git.kor-elf.net/kor-elf-shield/blocklist/parser"
)
/**
* An example of how to get a list of IP addresses from a service https://www.blocklist.de/en/export.html
*/
func main() {
// Getting a list of IP addresses that were entered in the last hour
// time=seconds
url := "https://api.blocklist.de/getlast.php?time=3600"
extract := parser.NewDefaultTextExtract(0, "\t")
pars, err := parser.NewText(extract)
if err != nil {
panic(err)
}
// limit 0 - no limit
limit := uint(0)
config := blocklist.NewConfig(limit)
// If you need to exclude an IP address or subnet
//excludeIPs := []string{
// "172.18.0.2",
// //"172.18.0.0/24",
// //"172.18.0.0-172.18.0.255",
//}
//exclusionChecker, err := parser.NewExclusionChecker(excludeIPs)
//if err != nil {
// panic(err)
//}
//config := blocklist.NewConfigWithExclusionChecker(limit, exclusionChecker)
// Get IPv4 and IPv6 addresses in one list
ips, err := blocklist.Get(url, pars, config)
if err != nil {
panic(err)
}
fmt.Println(ips)
// Get IPv4 and IPv6 addresses in two lists
ipsV4, ipsV6, err := blocklist.GetSeparatedIPs(url, pars, config)
if err != nil {
panic(err)
}
fmt.Println("IPv4")
fmt.Println(ipsV4)
fmt.Println("IPv6")
fmt.Println(ipsV6)
/*
// This second list retrieves all the IP addresses added in the last 48 hours and is usually a
// very large list (over 10000 entries), so be sure that you have the resources available to use it
url := "http://lists.blocklist.de/lists/all.txt"
extract := parser.NewDefaultTextExtract(0, "\t")
pars, err := parser.NewText(extract)
if err != nil {
panic(err)
}
// limit 0 - no limit
limit := uint(0)
config := blocklist.NewConfig(limit)
// If you need to exclude an IP address or subnet
//excludeIPs := []string{
// "172.18.0.2",
// //"172.18.0.0/24",
// //"172.18.0.0-172.18.0.255",
//}
//exclusionChecker, err := parser.NewExclusionChecker(excludeIPs)
//if err != nil {
// panic(err)
//}
//config := blocklist.NewConfigWithExclusionChecker(limit, exclusionChecker)
// Get IPv4 and IPv6 addresses in one list
ips, err := blocklist.Get(url, pars, config)
if err != nil {
panic(err)
}
fmt.Println(ips)
// Get IPv4 and IPv6 addresses in two lists
ipsV4, ipsV6, err := blocklist.GetSeparatedIPs(url, pars, config)
if err != nil {
panic(err)
}
fmt.Println("IPv4")
fmt.Println(ipsV4)
fmt.Println("IPv6")
fmt.Println(ipsV6)
*/
}
+53
View File
@@ -0,0 +1,53 @@
package main
import (
"fmt"
"git.kor-elf.net/kor-elf-shield/blocklist"
"git.kor-elf.net/kor-elf-shield/blocklist/parser"
)
/**
* An example of how to get a list of IP addresses from a service https://danger.rulez.sk/index.php/bruteforceblocker/
*/
func main() {
url := "https://danger.rulez.sk/projects/bruteforceblocker/blist.php"
extract := parser.NewDefaultTextExtract(0, "\t")
pars, err := parser.NewText(extract)
if err != nil {
panic(err)
}
// limit 0 - no limit
limit := uint(0)
config := blocklist.NewConfig(limit)
// If you need to exclude an IP address or subnet
//excludeIPs := []string{
// "172.18.0.2",
// //"172.18.0.0/24",
// //"172.18.0.0-172.18.0.255",
//}
//exclusionChecker, err := parser.NewExclusionChecker(excludeIPs)
//if err != nil {
// panic(err)
//}
//config := blocklist.NewConfigWithExclusionChecker(limit, exclusionChecker)
// Get IPv4 and IPv6 addresses in one list
ips, err := blocklist.Get(url, pars, config)
if err != nil {
panic(err)
}
fmt.Println(ips)
// Get IPv4 and IPv6 addresses in two lists
ipsV4, ipsV6, err := blocklist.GetSeparatedIPs(url, pars, config)
if err != nil {
panic(err)
}
fmt.Println("IPv4")
fmt.Println(ipsV4)
fmt.Println("IPv6")
fmt.Println(ipsV6)
}
+53
View File
@@ -0,0 +1,53 @@
package main
import (
"fmt"
"git.kor-elf.net/kor-elf-shield/blocklist"
"git.kor-elf.net/kor-elf-shield/blocklist/parser"
)
/**
* An example of how to get a list of IP addresses from a service https://www.ciarmy.com/#list
*/
func main() {
url := "https://www.ciarmy.com/list/ci-badguys.txt"
extract := parser.NewDefaultTextExtract(0, " ")
pars, err := parser.NewText(extract)
if err != nil {
panic(err)
}
// limit 0 - no limit
limit := uint(0)
config := blocklist.NewConfig(limit)
// If you need to exclude an IP address or subnet
//excludeIPs := []string{
// "172.18.0.2",
// //"172.18.0.0/24",
// //"172.18.0.0-172.18.0.255",
//}
//exclusionChecker, err := parser.NewExclusionChecker(excludeIPs)
//if err != nil {
// panic(err)
//}
//config := blocklist.NewConfigWithExclusionChecker(limit, exclusionChecker)
// Get IPv4 and IPv6 addresses in one list
ips, err := blocklist.Get(url, pars, config)
if err != nil {
panic(err)
}
fmt.Println(ips)
// Get IPv4 and IPv6 addresses in two lists
ipsV4, ipsV6, err := blocklist.GetSeparatedIPs(url, pars, config)
if err != nil {
panic(err)
}
fmt.Println("IPv4")
fmt.Println(ipsV4)
fmt.Println("IPv6")
fmt.Println(ipsV6)
}
+51 -1
View File
@@ -8,7 +8,7 @@ import (
)
/**
* An example of how to get a list of bad IP addresses from the service https://dshield.org/
* An example of how to get a list of IP addresses from a service https://dshield.org/
*/
func main() {
@@ -21,12 +21,36 @@ func main() {
// limit 0 - no limit
limit := uint(0)
config := blocklist.NewConfig(limit)
// If you need to exclude an IP address or subnet
//excludeIPs := []string{
// "172.18.0.2",
// //"172.18.0.0/24",
// //"172.18.0.0-172.18.0.255",
//}
//exclusionChecker, err := parser.NewExclusionChecker(excludeIPs)
//if err != nil {
// panic(err)
//}
//config := blocklist.NewConfigWithExclusionChecker(limit, exclusionChecker)
// Get IPv4 and IPv6 addresses in one list
ips, err := blocklist.Get(url, pars, config)
if err != nil {
panic(err)
}
fmt.Println(ips)
// Get IPv4 and IPv6 addresses in two lists
ipsV4, ipsV6, err := blocklist.GetSeparatedIPs(url, pars, config)
if err != nil {
panic(err)
}
fmt.Println("IPv4")
fmt.Println(ipsV4)
fmt.Println("IPv6")
fmt.Println(ipsV6)
/*
// You can also get a range of IP addresses from this service (from to)
url := "https://www.dshield.org/block.txt"
@@ -38,10 +62,36 @@ func main() {
// limit 0 - no limit
limit := uint(0)
config := blocklist.NewConfigWithValidator(limit, &parser.IPRangeValidator{})
// If you need to exclude an IP address or subnet
//excludeIPs := []string{
// "172.18.0.2",
// //"172.18.0.0/24",
// //"172.18.0.0-172.18.0.255",
//}
//exclusionChecker, err := parser.NewExclusionChecker(excludeIPs)
//if err != nil {
// panic(err)
//}
//config := blocklist.NewConfigWithValidator(limit, &parser.IPRangeValidator{
// ExclusionChecker: exclusionChecker,
//})
// Get IPv4 and IPv6 addresses in one list
ips, err := blocklist.Get(url, pars, config)
if err != nil {
panic(err)
}
fmt.Println(ips)
// Get IPv4 and IPv6 addresses in two lists
ipsV4, ipsV6, err := blocklist.GetSeparatedIPs(url, pars, config)
if err != nil {
panic(err)
}
fmt.Println("IPv4")
fmt.Println(ipsV4)
fmt.Println("IPv6")
fmt.Println(ipsV6)
*/
}
+53
View File
@@ -0,0 +1,53 @@
package main
import (
"fmt"
"git.kor-elf.net/kor-elf-shield/blocklist"
"git.kor-elf.net/kor-elf-shield/blocklist/parser"
)
/**
* An example of how to get a list of IP addresses from a service https://greensnow.co/
*/
func main() {
url := "https://blocklist.greensnow.co/greensnow.txt"
extract := parser.NewDefaultTextExtract(0, " ")
pars, err := parser.NewText(extract)
if err != nil {
panic(err)
}
// limit 0 - no limit
limit := uint(0)
config := blocklist.NewConfig(limit)
// If you need to exclude an IP address or subnet
//excludeIPs := []string{
// "172.18.0.2",
// //"172.18.0.0/24",
// //"172.18.0.0-172.18.0.255",
//}
//exclusionChecker, err := parser.NewExclusionChecker(excludeIPs)
//if err != nil {
// panic(err)
//}
//config := blocklist.NewConfigWithExclusionChecker(limit, exclusionChecker)
// Get IPv4 and IPv6 addresses in one list
ips, err := blocklist.Get(url, pars, config)
if err != nil {
panic(err)
}
fmt.Println(ips)
// Get IPv4 and IPv6 addresses in two lists
ipsV4, ipsV6, err := blocklist.GetSeparatedIPs(url, pars, config)
if err != nil {
panic(err)
}
fmt.Println("IPv4")
fmt.Println(ipsV4)
fmt.Println("IPv6")
fmt.Println(ipsV6)
}
+25 -1
View File
@@ -10,7 +10,7 @@ import (
)
/**
* An example of how to get a list of bad IP addresses from the service https://www.projecthoneypot.org/list_of_ips.php
* An example of how to get a list of IP addresses from a service https://www.projecthoneypot.org/list_of_ips.php
*/
type rssItem struct {
@@ -45,9 +45,33 @@ func main() {
// limit 0 - no limit
limit := uint(0)
config := blocklist.NewConfig(limit)
// If you need to exclude an IP address or subnet
//excludeIPs := []string{
// "172.18.0.2",
// //"172.18.0.0/24",
// //"172.18.0.0-172.18.0.255",
//}
//exclusionChecker, err := parser.NewExclusionChecker(excludeIPs)
//if err != nil {
// panic(err)
//}
//config := blocklist.NewConfigWithExclusionChecker(limit, exclusionChecker)
// Get IPv4 and IPv6 addresses in one list
ips, err := blocklist.Get(url, pars, config)
if err != nil {
panic(err)
}
fmt.Println(ips)
// Get IPv4 and IPv6 addresses in two lists
ipsV4, ipsV6, err := blocklist.GetSeparatedIPs(url, pars, config)
if err != nil {
panic(err)
}
fmt.Println("IPv4")
fmt.Println(ipsV4)
fmt.Println("IPv6")
fmt.Println(ipsV6)
}
+25 -1
View File
@@ -9,7 +9,7 @@ import (
)
/**
* An example of how to get a list of bad IP addresses from the service https://www.spamhaus.org/blocklists/do-not-route-or-peer/
* An example of how to get a list of IP addresses from a service https://www.spamhaus.org/blocklists/do-not-route-or-peer/
*/
type lineJson struct {
@@ -32,9 +32,33 @@ func main() {
// limit 0 - no limit
limit := uint(0)
config := blocklist.NewConfig(limit)
// If you need to exclude an IP address or subnet
//excludeIPs := []string{
// "172.18.0.2",
// //"172.18.0.0/24",
// //"172.18.0.0-172.18.0.255",
//}
//exclusionChecker, err := parser.NewExclusionChecker(excludeIPs)
//if err != nil {
// panic(err)
//}
//config := blocklist.NewConfigWithExclusionChecker(limit, exclusionChecker)
// Get IPv4 and IPv6 addresses in one list
ips, err := blocklist.Get(url, pars, config)
if err != nil {
panic(err)
}
fmt.Println(ips)
// Get IPv4 and IPv6 addresses in two lists
ipsV4, ipsV6, err := blocklist.GetSeparatedIPs(url, pars, config)
if err != nil {
panic(err)
}
fmt.Println("IPv4")
fmt.Println(ipsV4)
fmt.Println("IPv6")
fmt.Println(ipsV6)
}
+56
View File
@@ -0,0 +1,56 @@
package main
import (
"fmt"
"git.kor-elf.net/kor-elf-shield/blocklist"
"git.kor-elf.net/kor-elf-shield/blocklist/parser"
)
/**
* An example of how to get a list of IP addresses from a service https://www.stopforumspam.com/downloads
*/
func main() {
url := "https://www.stopforumspam.com/downloads/listed_ip_1.zip"
//url := "https://www.stopforumspam.com/downloads/listed_ip_1_ipv6.zip"
extract := parser.NewDefaultTextExtract(0, " ")
pars, err := parser.NewText(extract)
if err != nil {
panic(err)
}
// limit 0 - no limit
limit := uint(0)
config := blocklist.NewConfig(limit)
// If you need to exclude an IP address or subnet
//excludeIPs := []string{
// "172.18.0.2",
// //"172.18.0.0/24",
// //"172.18.0.0-172.18.0.255",
//}
//exclusionChecker, err := parser.NewExclusionChecker(excludeIPs)
//if err != nil {
// panic(err)
//}
//config := blocklist.NewConfigWithExclusionChecker(limit, exclusionChecker)
configZip := blocklist.NewConfigZip(config)
// Get IPv4 and IPv6 addresses in one list
ips, err := blocklist.GetZip(url, pars, configZip)
if err != nil {
panic(err)
}
fmt.Println(ips)
// Get IPv4 and IPv6 addresses in two lists
ipsV4, ipsV6, err := blocklist.GetZipSeparatedIPs(url, pars, configZip)
if err != nil {
panic(err)
}
fmt.Println("IPv4")
fmt.Println(ipsV4)
fmt.Println("IPv6")
fmt.Println(ipsV6)
}
+53
View File
@@ -0,0 +1,53 @@
package main
import (
"fmt"
"git.kor-elf.net/kor-elf-shield/blocklist"
"git.kor-elf.net/kor-elf-shield/blocklist/parser"
)
/**
* An example of how to get a list of IP addresses from a service https://check.torproject.org/torbulkexitlist
*/
func main() {
url := "https://check.torproject.org/torbulkexitlist"
extract := parser.NewDefaultTextExtract(0, " ")
pars, err := parser.NewText(extract)
if err != nil {
panic(err)
}
// limit 0 - no limit
limit := uint(0)
config := blocklist.NewConfig(limit)
// If you need to exclude an IP address or subnet
//excludeIPs := []string{
// "172.18.0.2",
// //"172.18.0.0/24",
// //"172.18.0.0-172.18.0.255",
//}
//exclusionChecker, err := parser.NewExclusionChecker(excludeIPs)
//if err != nil {
// panic(err)
//}
//config := blocklist.NewConfigWithExclusionChecker(limit, exclusionChecker)
// Get IPv4 and IPv6 addresses in one list
ips, err := blocklist.Get(url, pars, config)
if err != nil {
panic(err)
}
fmt.Println(ips)
// Get IPv4 and IPv6 addresses in two lists
ipsV4, ipsV6, err := blocklist.GetSeparatedIPs(url, pars, config)
if err != nil {
panic(err)
}
fmt.Println("IPv4")
fmt.Println(ipsV4)
fmt.Println("IPv6")
fmt.Println(ipsV6)
}
+294
View File
@@ -0,0 +1,294 @@
package parser
import (
"encoding/binary"
"fmt"
"math/big"
"net/netip"
"strings"
)
// ExclusionChecker is an interface for checking if an IP is excluded.
type ExclusionChecker interface {
IsExcluded(ip string) (excluded bool, ips []string, err error)
}
type ipRange struct {
start netip.Addr
end netip.Addr
isV4 bool
}
type exclusionChecker struct {
ipRanges []ipRange
}
// NewExclusionChecker creates a new exclusion checker.
func NewExclusionChecker(ips []string) (ExclusionChecker, error) {
ipRanges := make([]ipRange, 0, len(ips))
for _, ip := range ips {
ipRange, err := parseSpec(ip)
if err != nil {
return nil, fmt.Errorf("invalid exclusion IP: %s", ip)
}
ipRanges = append(ipRanges, ipRange)
}
return &exclusionChecker{
ipRanges: ipRanges,
}, nil
}
// IsExcluded checks if the given IP is excluded.
func (e *exclusionChecker) IsExcluded(ip string) (excluded bool, ips []string, err error) {
inIPRange, err := parseSpec(ip)
if err != nil {
return false, nil, err
}
pending := []ipRange{inIPRange}
matchedAny := false
for _, baseRange := range e.ipRanges {
// Different IP families do not overlap.
if baseRange.isV4 != inIPRange.isV4 {
continue
}
nextPending := make([]ipRange, 0, len(pending))
for _, candidate := range pending {
matched, remains := subtractRange(candidate, baseRange)
if matched {
matchedAny = true
}
nextPending = append(nextPending, remains...)
}
pending = nextPending
if len(pending) == 0 {
// Completely excluded the range.
return true, []string{}, nil
}
}
if !matchedAny {
return false, nil, nil
}
out := make([]string, 0, len(pending))
for _, r := range pending {
out = append(out, formatRange(r))
}
return true, out, nil
}
func parseSpec(s string) (ipRange, error) {
s = strings.TrimSpace(s)
if s == "" {
return ipRange{}, fmt.Errorf("empty value")
}
// range: a-b
if strings.Contains(s, "-") {
parts := strings.Split(s, "-")
if len(parts) != 2 {
return ipRange{}, fmt.Errorf("invalid range format")
}
a, err := parseAddr(strings.TrimSpace(parts[0]))
if err != nil {
return ipRange{}, err
}
b, err := parseAddr(strings.TrimSpace(parts[1]))
if err != nil {
return ipRange{}, err
}
if a.Is4() != b.Is4() {
return ipRange{}, fmt.Errorf("mixed ip families in range")
}
if compareAddr(a, b) > 0 {
return ipRange{}, fmt.Errorf("range start > end")
}
return ipRange{start: a, end: b, isV4: a.Is4()}, nil
}
// cidr
if strings.Contains(s, "/") {
p, err := netip.ParsePrefix(s)
if err != nil {
return ipRange{}, fmt.Errorf("invalid cidr: %w", err)
}
p = p.Masked()
start := p.Addr()
var end netip.Addr
if start.Is4() {
u := ip4ToU32(start)
hostBits := uint32(32 - p.Bits())
var mask uint32
if hostBits == 32 {
mask = ^uint32(0)
} else {
mask = (uint32(1) << hostBits) - 1
}
end = u32ToIP4(u | mask)
return ipRange{start: start, end: end, isV4: true}, nil
}
u := ip16ToBig(start)
hostBits := 128 - p.Bits()
ones := new(big.Int).Lsh(big.NewInt(1), uint(hostBits))
ones.Sub(ones, big.NewInt(1))
u.Or(u, ones)
end = bigToIP16(u)
return ipRange{start: start, end: end, isV4: false}, nil
}
// single ip
a, err := parseAddr(s)
if err != nil {
return ipRange{}, err
}
return ipRange{start: a, end: a, isV4: a.Is4()}, nil
}
func parseAddr(s string) (netip.Addr, error) {
a, err := netip.ParseAddr(s)
if err != nil {
return netip.Addr{}, fmt.Errorf("invalid ip: %w", err)
}
return a.Unmap(), nil // important for ::ffff:1.2.3.4
}
func compareAddr(a, b netip.Addr) int {
if a.Is4() && b.Is4() {
ua := ip4ToU32(a)
ub := ip4ToU32(b)
switch {
case ua < ub:
return -1
case ua > ub:
return 1
default:
return 0
}
}
aa := a.As16()
bb := b.As16()
for i := 0; i < 16; i++ {
if aa[i] < bb[i] {
return -1
}
if aa[i] > bb[i] {
return 1
}
}
return 0
}
func ip4ToU32(a netip.Addr) uint32 {
v := a.As4()
return binary.BigEndian.Uint32(v[:])
}
func u32ToIP4(u uint32) netip.Addr {
var b [4]byte
binary.BigEndian.PutUint32(b[:], u)
return netip.AddrFrom4(b)
}
func ip16ToBig(a netip.Addr) *big.Int {
v := a.As16()
return new(big.Int).SetBytes(v[:])
}
func bigToIP16(x *big.Int) netip.Addr {
var b [16]byte
raw := x.Bytes()
copy(b[16-len(raw):], raw)
return netip.AddrFrom16(b)
}
func subtractRange(incoming, base ipRange) (bool, []ipRange) {
// No intersection
if compareAddr(base.end, incoming.start) < 0 || compareAddr(base.start, incoming.end) > 0 {
return false, []ipRange{incoming}
}
// Full incoming coverage
if compareAddr(base.start, incoming.start) <= 0 && compareAddr(base.end, incoming.end) >= 0 {
return true, nil
}
remains := make([]ipRange, 0, 2)
// Left side
if compareAddr(base.start, incoming.start) > 0 {
leftEnd, ok := prevIP(base.start)
if ok && compareAddr(incoming.start, leftEnd) <= 0 {
remains = append(remains, ipRange{
start: incoming.start,
end: leftEnd,
isV4: incoming.isV4,
})
}
}
// Right side
if compareAddr(base.end, incoming.end) < 0 {
rightStart, ok := nextIP(base.end)
if ok && compareAddr(rightStart, incoming.end) <= 0 {
remains = append(remains, ipRange{
start: rightStart,
end: incoming.end,
isV4: incoming.isV4,
})
}
}
return true, remains
}
func nextIP(a netip.Addr) (netip.Addr, bool) {
if a.Is4() {
u := ip4ToU32(a)
if u == ^uint32(0) {
return netip.Addr{}, false
}
return u32ToIP4(u + 1), true
}
u := ip16ToBig(a)
max := new(big.Int).Lsh(big.NewInt(1), 128)
max.Sub(max, big.NewInt(1))
if u.Cmp(max) == 0 {
return netip.Addr{}, false
}
u.Add(u, big.NewInt(1))
return bigToIP16(u), true
}
func prevIP(a netip.Addr) (netip.Addr, bool) {
if a.Is4() {
u := ip4ToU32(a)
if u == 0 {
return netip.Addr{}, false
}
return u32ToIP4(u - 1), true
}
u := ip16ToBig(a)
if u.Sign() == 0 {
return netip.Addr{}, false
}
u.Sub(u, big.NewInt(1))
return bigToIP16(u), true
}
func formatRange(r ipRange) string {
if r.start == r.end {
return r.start.String()
}
return r.start.String() + "-" + r.end.String()
}
+57 -1
View File
@@ -55,7 +55,10 @@ func (p *jsonLinesParser) Parse(body io.Reader, validator IPValidator, limit uin
continue
}
ips = append(ips, ip)
ips, err = appendIfNotExcluded(ip, ips, validator)
if err != nil {
return nil, err
}
if limit > 0 && uint(len(ips)) >= limit {
break
}
@@ -63,3 +66,56 @@ func (p *jsonLinesParser) Parse(body io.Reader, validator IPValidator, limit uin
return ips, nil
}
// ParseIPsByVersion parses the JSON Lines data from the given reader
// and returns a slice of IP addresses for each IP version.
// It also returns any errors that occurred during the process.
func (p *jsonLinesParser) ParseIPsByVersion(body io.Reader, validator IPValidator, limit uint) (ipV4 IPs, ipV6 IPs, err error) {
decoder := json.NewDecoder(body)
ipV4 = make(IPs, 0)
ipV6 = make(IPs, 0)
for {
var item json.RawMessage
if err := decoder.Decode(&item); err != nil {
if err == io.EOF {
break
}
return nil, nil, fmt.Errorf("decode json item: %w", err)
}
if item == nil {
continue
}
ip, err := p.extract(item)
if err != nil {
return nil, nil, fmt.Errorf("extract ip: %w", err)
}
ip = strings.TrimSpace(ip)
isValid, ipVersion := validator.IsValidAndReturnVersion(ip)
if !isValid {
continue
}
if ipVersion == IPVersion4 {
ipV4, err = appendIfNotExcluded(ip, ipV4, validator)
if err != nil {
return nil, nil, err
}
} else if ipVersion == IPVersion6 {
ipV6, err = appendIfNotExcluded(ip, ipV6, validator)
if err != nil {
return nil, nil, err
}
} else {
continue
}
if limit > 0 && uint(len(ipV4))+uint(len(ipV6)) >= limit {
break
}
}
return ipV4, ipV6, nil
}
+117 -2
View File
@@ -11,20 +11,37 @@ import (
type Parser interface {
// Parse reads the body and returns a slice of IP addresses.
Parse(body io.Reader, validator IPValidator, limit uint) (IPs, error)
ParseIPsByVersion(body io.Reader, validator IPValidator, limit uint) (ipV4 IPs, ipV6 IPs, err error)
}
// IPValidator interface defines the contract for validating IP addresses.
type IPValidator interface {
// IsValid checks if the given IP address is valid.
IsValid(ip string) bool
// IsValidAndReturnVersion validates the given IP address and returns whether it is valid and its version (IPv4 or IPv6).
IsValidAndReturnVersion(ip string) (bool, IPVersion)
// IsExcluded determines if the given IP address is excluded based on certain criteria and returns related IP addresses.
IsExcluded(ip string) (excluded bool, ips []string, err error)
}
type IPVersion int
const (
IPVersion4 IPVersion = iota
IPVersion6
)
// IPs is a slice of IP addresses.
type IPs []string
// DefaultIPValidator implements IPValidator interface.
// It validates IP addresses by parsing them using net.ParseIP and net.ParseCIDR.
type DefaultIPValidator struct{}
type DefaultIPValidator struct {
ExclusionChecker ExclusionChecker
}
// IsValid checks if the given IP address is valid.
// It returns true if the IP address is not a loopback address.
@@ -50,9 +67,60 @@ func (v *DefaultIPValidator) IsValid(value string) bool {
return false
}
// IsValidAndReturnVersion checks if the given IP address is valid and returns the IP version.
// It returns true if the IP address is not a loopback address and the IP version is either IPv4 or IPv6.
func (v *DefaultIPValidator) IsValidAndReturnVersion(value string) (bool, IPVersion) {
if value == "" {
return false, IPVersion4
}
if ip := net.ParseIP(value); ip != nil {
if ip.IsLoopback() {
return false, IPVersion4
}
if ip.To4() != nil {
return true, IPVersion4
}
if ip.To16() != nil {
return true, IPVersion6
}
return false, IPVersion4
}
if ip, _, err := net.ParseCIDR(value); err == nil {
if ip.IsLoopback() {
return false, IPVersion4
}
if ip.To4() != nil {
return true, IPVersion4
}
if ip.To16() != nil {
return true, IPVersion6
}
return false, IPVersion4
}
return false, IPVersion4
}
func (v *DefaultIPValidator) IsExcluded(ip string) (excluded bool, ips []string, err error) {
if v.ExclusionChecker == nil {
return false, nil, nil
}
return v.ExclusionChecker.IsExcluded(ip)
}
// IPRangeValidator implements IPValidator interface.
// It validates IP ranges by parsing them using net.ParseIP and checking if the start and end IPs are in the same network.
type IPRangeValidator struct{}
type IPRangeValidator struct {
ExclusionChecker ExclusionChecker
}
// IsValid checks if the given IP range is valid.
// It returns true if the start and end IPs are in the same network.
@@ -89,3 +157,50 @@ func (v *IPRangeValidator) IsValid(value string) bool {
return false
}
}
func (v *IPRangeValidator) IsValidAndReturnVersion(value string) (bool, IPVersion) {
if value == "" {
return false, IPVersion4
}
parts := strings.Split(value, "-")
if len(parts) != 2 {
return false, IPVersion4
}
start := net.ParseIP(strings.TrimSpace(parts[0]))
end := net.ParseIP(strings.TrimSpace(parts[1]))
if start == nil || end == nil {
return false, IPVersion4
}
start4 := start.To4()
end4 := end.To4()
switch {
case start4 != nil && end4 != nil:
if bytes.Compare(start4, end4) <= 0 {
return true, IPVersion4
}
return false, IPVersion4
case start4 == nil && end4 == nil:
start16 := start.To16()
end16 := end.To16()
if start16 == nil || end16 == nil {
return false, IPVersion4
}
if bytes.Compare(start16, end16) <= 0 {
return true, IPVersion6
}
return false, IPVersion4
default:
return false, IPVersion4
}
}
func (v *IPRangeValidator) IsExcluded(ip string) (excluded bool, ips []string, err error) {
if v.ExclusionChecker == nil {
return false, nil, nil
}
return v.ExclusionChecker.IsExcluded(ip)
}
+16
View File
@@ -0,0 +1,16 @@
package parser
func appendIfNotExcluded(ip string, ips []string, validator IPValidator) ([]string, error) {
if exclude, listIP, err := validator.IsExcluded(ip); err != nil {
return ips, err
} else if exclude {
if listIP == nil || len(listIP) == 0 {
return ips, nil
}
ips = append(ips, listIP...)
return ips, nil
}
ips = append(ips, ip)
return ips, nil
}
+60 -1
View File
@@ -58,7 +58,10 @@ func (p *rssParser) Parse(body io.Reader, validator IPValidator, limit uint) (IP
continue
}
ips = append(ips, ip)
ips, err = appendIfNotExcluded(ip, ips, validator)
if err != nil {
return nil, err
}
if limit > 0 && uint(len(ips)) >= limit {
break
}
@@ -66,3 +69,59 @@ func (p *rssParser) Parse(body io.Reader, validator IPValidator, limit uint) (IP
return ips, nil
}
// ParseIPsByVersion parses the RSS data from the given reader
// and returns a slice of IP addresses for each IP version.
// It also returns any errors that occurred during the process.
func (p *rssParser) ParseIPsByVersion(body io.Reader, validator IPValidator, limit uint) (ipV4 IPs, ipV6 IPs, err error) {
decoder := xml.NewDecoder(body)
ipV4 = make(IPs, 0)
ipV6 = make(IPs, 0)
for {
token, err := decoder.Token()
if err != nil {
if err == io.EOF {
break
}
return nil, nil, fmt.Errorf("parse rss: %w", err)
}
start, ok := token.(xml.StartElement)
if !ok {
continue
}
ip, err := p.extract(decoder, start)
if err != nil {
return nil, nil, fmt.Errorf("extract rss ip: %w", err)
}
ip = strings.TrimSpace(ip)
isValid, ipVersion := validator.IsValidAndReturnVersion(ip)
if !isValid {
continue
}
if ipVersion == IPVersion4 {
ipV4, err = appendIfNotExcluded(ip, ipV4, validator)
if err != nil {
return nil, nil, err
}
} else if ipVersion == IPVersion6 {
ipV6, err = appendIfNotExcluded(ip, ipV6, validator)
if err != nil {
return nil, nil, err
}
} else {
continue
}
if limit > 0 && uint(len(ipV4))+uint(len(ipV6)) >= limit {
break
}
}
return ipV4, ipV6, nil
}
+57 -1
View File
@@ -146,7 +146,11 @@ func (p *textParser) Parse(body io.Reader, validator IPValidator, limit uint) (I
continue
}
ips = append(ips, ip)
var err error
ips, err = appendIfNotExcluded(ip, ips, validator)
if err != nil {
return nil, err
}
if limit > 0 && uint(len(ips)) >= limit {
break
}
@@ -158,3 +162,55 @@ func (p *textParser) Parse(body io.Reader, validator IPValidator, limit uint) (I
return ips, nil
}
func (p *textParser) ParseIPsByVersion(body io.Reader, validator IPValidator, limit uint) (ipV4 IPs, ipV6 IPs, err error) {
ipV4 = make(IPs, 0)
ipV6 = make(IPs, 0)
scanner := bufio.NewScanner(body)
buf := make([]byte, 0, 64*1024)
scanner.Buffer(buf, 1024*1024)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, ";") || strings.HasPrefix(line, "#") {
continue
}
ip, isFound := p.textExtract.Extract(line)
if !isFound {
continue
}
ip = strings.TrimSpace(ip)
isValid, ipVersion := validator.IsValidAndReturnVersion(ip)
if !isValid {
continue
}
if ipVersion == IPVersion4 {
ipV4, err = appendIfNotExcluded(ip, ipV4, validator)
if err != nil {
return nil, nil, err
}
} else if ipVersion == IPVersion6 {
ipV6, err = appendIfNotExcluded(ip, ipV6, validator)
if err != nil {
return nil, nil, err
}
} else {
continue
}
if limit > 0 && uint(len(ipV4))+uint(len(ipV6)) >= limit {
break
}
}
if err := scanner.Err(); err != nil {
return nil, nil, fmt.Errorf("read response: %w", err)
}
return ipV4, ipV6, nil
}