Skip to content

Commit

Permalink
Feat: add lookup command
Browse files Browse the repository at this point in the history
  • Loading branch information
Loyalsoldier committed Jul 18, 2024
1 parent 130d27a commit 1a184a8
Show file tree
Hide file tree
Showing 5 changed files with 483 additions and 31 deletions.
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ These two concepts are notable: `input` and `output`. The `input` is the data so

- **text**:纯文本 CIDR(例如:`1.0.0.0/24`)
- **stdout**:将纯文本 CIDR 输出到 standard output(例如:`1.0.0.0/24`)
- **lookup**:从指定的列表中查找指定的 IP 或 CIDR
- **v2rayGeoIPDat**:V2Ray GeoIP dat 格式(`geoip.dat`,适用于 [V2Ray](https://github.com/v2fly/v2ray-core)、[Xray-core](https://github.com/XTLS/Xray-core) 和 [Trojan-Go](https://github.com/p4gefau1t/trojan-go))
- **maxmindMMDB**:MaxMind mmdb 数据格式(`GeoLite2-Country.mmdb`,适用于 [Clash](https://github.com/Dreamacro/clash) 和 [Leaf](https://github.com/eycorsican/leaf))
- **singboxSRS**:sing-box SRS 格式(`geoip-cn.srs`,适用于 [sing-box](https://github.com/SagerNet/sing-box))
Expand Down Expand Up @@ -237,6 +238,7 @@ Available Commands:
convert Convert geoip data from one format to another by using config file
help Help about any command
list List all available input and output formats
lookup Lookup specified IP or CIDR in specified lists
merge Merge plaintext IP & CIDR from standard input, then print to standard output
Flags:
Expand Down Expand Up @@ -264,6 +266,7 @@ All available input formats:
All available output formats:
- clashRuleSet (Convert data to ipcidr type of Clash RuleSet)
- clashRuleSetClassical (Convert data to classical type of Clash RuleSet)
- lookup (Lookup specified IP or CIDR from various formats of data)
- maxmindMMDB (Convert data to MaxMind mmdb database format)
- singboxSRS (Convert data to sing-box SRS format)
- stdout (Convert data to plaintext CIDR format and output to standard output)
Expand Down Expand Up @@ -308,6 +311,55 @@ $ ./geoip convert -c config.json
2021/08/29 12:11:45 ✅ [singboxSRS] fastly.txt --> output/srs
```

```bash
# lookup one IP from local file
$ ./geoip lookup -f text -u ./cn.txt -n cn 1.0.1.1
cn
# lookup one CIDR from local file
$ ./geoip lookup -f text -u ./cn.txt -n cn 1.0.1.1/24
cn
# lookup IP or CIDR in REPL mode from local file
$ ./geoip lookup -f text -u ./cn.txt -n cn
Enter IP or CIDR (type `exit` to quit):
>> 1.0.1.1
cn
>> 1.0.1.1/24
cn

# lookup IP or CIDR in REPL mode from remote file
$ ./geoip lookup -f text -u https://example.com/cn.txt -n cn
Enter IP or CIDR (type `exit` to quit):
>> 1.0.1.1
cn
>> 1.0.1.1/24
cn

# lookup IP or CIDR in REPL mode from local directory, got two lists joined with comma
$ ./geoip lookup -f text -d ./path/to/your/directory/
Enter IP or CIDR (type `exit` to quit):
>> 1.0.1.1
cn,my-custom-list
>> 1.0.1.1/24
cn,my-custom-list

# lookup IP or CIDR in REPL mode from specified lists in local directory
$ ./geoip lookup -f text -d ./path/to/your/directory/ -l cn,us,jp
>> 1.0.1.1
cn
>> 1.0.1.1/24
cn

# lookup IP or CIDR in REPL mode with another format from specified lists in remote file
$ ./geoip lookup -f v2rayGeoIPDat -u https://example.com/geoip.dat -l cn,us,jp
>> 1.0.1.1
cn
>> 1.0.1.1/24
cn
```

## License

[CC-BY-SA-4.0](https://creativecommons.org/licenses/by-sa/4.0/)
Expand Down
92 changes: 92 additions & 0 deletions lib/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package lib

import (
"fmt"
"net/netip"
"strings"

"go4.org/netipx"
Expand All @@ -12,6 +13,7 @@ type Container interface {
Add(entry *Entry, opts ...IgnoreIPOption) error
Remove(entry *Entry, rCase CaseRemove, opts ...IgnoreIPOption) error
Loop() <-chan *Entry
Lookup(ipOrCidr string, searchList ...string) ([]string, bool, error)
}

type container struct {
Expand Down Expand Up @@ -181,3 +183,93 @@ func (c *container) Remove(entry *Entry, rCase CaseRemove, opts ...IgnoreIPOptio

return nil
}

func (c *container) Lookup(ipOrCidr string, searchList ...string) ([]string, bool, error) {
switch strings.Contains(ipOrCidr, "/") {
case true: // CIDR
prefix, err := netip.ParsePrefix(ipOrCidr)
if err != nil {
return nil, false, err
}
addr := prefix.Addr().Unmap()
switch {
case addr.Is4():
return c.lookup(prefix, IPv4, searchList...)
case addr.Is6():
return c.lookup(prefix, IPv6, searchList...)
}

case false: // IP
addr, err := netip.ParseAddr(ipOrCidr)
if err != nil {
return nil, false, err
}
addr = addr.Unmap()
switch {
case addr.Is4():
return c.lookup(addr, IPv4, searchList...)
case addr.Is6():
return c.lookup(addr, IPv6, searchList...)
}
}

return nil, false, nil
}

func (c *container) lookup(addrOrPrefix any, iptype IPType, searchList ...string) ([]string, bool, error) {
searchMap := make(map[string]bool)
for _, name := range searchList {
if name = strings.ToUpper(strings.TrimSpace(name)); name != "" {
searchMap[name] = true
}
}

isfound := false
result := make([]string, 0, 8)

for entry := range c.Loop() {
if len(searchMap) > 0 && !searchMap[entry.GetName()] {
continue
}

switch iptype {
case IPv4:
ipset, err := entry.GetIPv4Set()
if err != nil {
return nil, false, err
}
switch addrOrPrefix := addrOrPrefix.(type) {
case netip.Prefix:
if found := ipset.ContainsPrefix(addrOrPrefix); found {
isfound = true
result = append(result, entry.GetName())
}
case netip.Addr:
if found := ipset.Contains(addrOrPrefix); found {
isfound = true
result = append(result, entry.GetName())
}
}

case IPv6:
ipset, err := entry.GetIPv6Set()
if err != nil {
return nil, false, err
}
switch addrOrPrefix := addrOrPrefix.(type) {
case netip.Prefix:
if found := ipset.ContainsPrefix(addrOrPrefix); found {
isfound = true
result = append(result, entry.GetName())
}
case netip.Addr:
if found := ipset.Contains(addrOrPrefix); found {
isfound = true
result = append(result, entry.GetName())
}
}
}
}

return result, isfound, nil
}
135 changes: 104 additions & 31 deletions lib/entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,21 @@ import (
"net"
"net/netip"
"strings"
"sync"

"go4.org/netipx"
)

type Entry struct {
name string
mu *sync.Mutex
ipv4Builder *netipx.IPSetBuilder
ipv6Builder *netipx.IPSetBuilder
ipv4Set *netipx.IPSet
ipv6Set *netipx.IPSet
}

func NewEntry(name string) *Entry {
return &Entry{
name: strings.ToUpper(strings.TrimSpace(name)),
mu: new(sync.Mutex),
ipv4Builder: new(netipx.IPSetBuilder),
ipv6Builder: new(netipx.IPSetBuilder),
name: strings.ToUpper(strings.TrimSpace(name)),
}
}

Expand All @@ -38,6 +35,38 @@ func (e *Entry) hasIPv6Builder() bool {
return e.ipv6Builder != nil
}

func (e *Entry) hasIPv4Set() bool {
return e.ipv4Set != nil
}

func (e *Entry) hasIPv6Set() bool {
return e.ipv6Set != nil
}

func (e *Entry) GetIPv4Set() (*netipx.IPSet, error) {
if err := e.buildIPSet(); err != nil {
return nil, err
}

if e.hasIPv4Set() {
return e.ipv4Set, nil
}

return nil, fmt.Errorf("entry %s has no ipv4 set", e.GetName())
}

func (e *Entry) GetIPv6Set() (*netipx.IPSet, error) {
if err := e.buildIPSet(); err != nil {
return nil, err
}

if e.hasIPv6Set() {
return e.ipv6Set, nil
}

return nil, fmt.Errorf("entry %s has no ipv6 set", e.GetName())
}

func (e *Entry) processPrefix(src any) (*netip.Prefix, IPType, error) {
switch src := src.(type) {
case net.IP:
Expand Down Expand Up @@ -218,9 +247,6 @@ func (e *Entry) processPrefix(src any) (*netip.Prefix, IPType, error) {
}

func (e *Entry) add(prefix *netip.Prefix, ipType IPType) error {
e.mu.Lock()
defer e.mu.Unlock()

switch ipType {
case IPv4:
if !e.hasIPv4Builder() {
Expand All @@ -240,9 +266,6 @@ func (e *Entry) add(prefix *netip.Prefix, ipType IPType) error {
}

func (e *Entry) remove(prefix *netip.Prefix, ipType IPType) error {
e.mu.Lock()
defer e.mu.Unlock()

switch ipType {
case IPv4:
if e.hasIPv4Builder() {
Expand Down Expand Up @@ -281,7 +304,27 @@ func (e *Entry) RemovePrefix(cidr string) error {
return nil
}

func (e *Entry) MarshalText(opts ...IgnoreIPOption) ([]string, error) {
func (e *Entry) buildIPSet() error {
if e.hasIPv4Builder() && !e.hasIPv4Set() {
ipv4set, err := e.ipv4Builder.IPSet()
if err != nil {
return err
}
e.ipv4Set = ipv4set
}

if e.hasIPv6Builder() && !e.hasIPv6Set() {
ipv6set, err := e.ipv6Builder.IPSet()
if err != nil {
return err
}
e.ipv6Set = ipv6set
}

return nil
}

func (e *Entry) MarshalPrefix(opts ...IgnoreIPOption) ([]netip.Prefix, error) {
var ignoreIPType IPType
for _, opt := range opts {
if opt != nil {
Expand All @@ -296,32 +339,62 @@ func (e *Entry) MarshalText(opts ...IgnoreIPOption) ([]string, error) {
disableIPv6 = true
}

prefixSet := make([]string, 0, 1024)
if err := e.buildIPSet(); err != nil {
return nil, err
}

if !disableIPv4 && e.hasIPv4Builder() {
ipv4set, err := e.ipv4Builder.IPSet()
if err != nil {
return nil, err
}
prefixes := ipv4set.Prefixes()
for _, prefix := range prefixes {
prefixSet = append(prefixSet, prefix.String())
prefixes := make([]netip.Prefix, 0, 1024)

if !disableIPv4 && e.hasIPv4Set() {
prefixes = append(prefixes, e.ipv4Set.Prefixes()...)
}

if !disableIPv6 && e.hasIPv6Set() {
prefixes = append(prefixes, e.ipv6Set.Prefixes()...)
}

if len(prefixes) > 0 {
return prefixes, nil
}

return nil, fmt.Errorf("entry %s has no prefix", e.GetName())
}

func (e *Entry) MarshalText(opts ...IgnoreIPOption) ([]string, error) {
var ignoreIPType IPType
for _, opt := range opts {
if opt != nil {
ignoreIPType = opt()
}
}
disableIPv4, disableIPv6 := false, false
switch ignoreIPType {
case IPv4:
disableIPv4 = true
case IPv6:
disableIPv6 = true
}

if !disableIPv6 && e.hasIPv6Builder() {
ipv6set, err := e.ipv6Builder.IPSet()
if err != nil {
return nil, err
if err := e.buildIPSet(); err != nil {
return nil, err
}

cidrList := make([]string, 0, 1024)

if !disableIPv4 && e.hasIPv4Set() {
for _, prefix := range e.ipv4Set.Prefixes() {
cidrList = append(cidrList, prefix.String())
}
prefixes := ipv6set.Prefixes()
for _, prefix := range prefixes {
prefixSet = append(prefixSet, prefix.String())
}

if !disableIPv6 && e.hasIPv6Set() {
for _, prefix := range e.ipv6Set.Prefixes() {
cidrList = append(cidrList, prefix.String())
}
}

if len(prefixSet) > 0 {
return prefixSet, nil
if len(cidrList) > 0 {
return cidrList, nil
}

return nil, fmt.Errorf("entry %s has no prefix", e.GetName())
Expand Down
Loading

0 comments on commit 1a184a8

Please sign in to comment.