From 6b3fa2500e58404c243399106860e641afd2fdc7 Mon Sep 17 00:00:00 2001 From: Andrevich <47223721+1andrevich@users.noreply.github.com> Date: Sat, 28 Sep 2024 19:59:11 +0400 Subject: [PATCH] second commit --- refilter-domain/go.mod | 14 + refilter-domain/go.sum | 22 + refilter-domain/main.go | 391 ++++++++++++++++ refilter-geoip/config-example.json | 146 ++++++ refilter-geoip/config.json | 27 ++ refilter-geoip/go.mod | 28 ++ refilter-geoip/go.sum | 175 +++++++ refilter-geoip/init.go | 8 + refilter-geoip/lib/config.go | 128 ++++++ refilter-geoip/lib/error.go | 13 + refilter-geoip/lib/func.go | 43 ++ refilter-geoip/lib/instance.go | 65 +++ refilter-geoip/lib/lib.go | 458 +++++++++++++++++++ refilter-geoip/main.go | 41 ++ refilter-geoip/plugin/maxmind/country_csv.go | 214 +++++++++ refilter-geoip/plugin/maxmind/mmdb_in.go | 227 +++++++++ refilter-geoip/plugin/plaintext/text_in.go | 227 +++++++++ refilter-geoip/plugin/plaintext/text_out.go | 166 +++++++ refilter-geoip/plugin/special/cutter.go | 96 ++++ refilter-geoip/plugin/special/private.go | 94 ++++ refilter-geoip/plugin/special/test.go | 74 +++ refilter-geoip/plugin/v2ray/dat_in.go | 204 +++++++++ refilter-geoip/plugin/v2ray/dat_out.go | 227 +++++++++ 23 files changed, 3088 insertions(+) create mode 100644 refilter-domain/go.mod create mode 100644 refilter-domain/go.sum create mode 100644 refilter-domain/main.go create mode 100644 refilter-geoip/config-example.json create mode 100644 refilter-geoip/config.json create mode 100644 refilter-geoip/go.mod create mode 100644 refilter-geoip/go.sum create mode 100644 refilter-geoip/init.go create mode 100644 refilter-geoip/lib/config.go create mode 100644 refilter-geoip/lib/error.go create mode 100644 refilter-geoip/lib/func.go create mode 100644 refilter-geoip/lib/instance.go create mode 100644 refilter-geoip/lib/lib.go create mode 100644 refilter-geoip/main.go create mode 100644 refilter-geoip/plugin/maxmind/country_csv.go create mode 100644 refilter-geoip/plugin/maxmind/mmdb_in.go create mode 100644 refilter-geoip/plugin/plaintext/text_in.go create mode 100644 refilter-geoip/plugin/plaintext/text_out.go create mode 100644 refilter-geoip/plugin/special/cutter.go create mode 100644 refilter-geoip/plugin/special/private.go create mode 100644 refilter-geoip/plugin/special/test.go create mode 100644 refilter-geoip/plugin/v2ray/dat_in.go create mode 100644 refilter-geoip/plugin/v2ray/dat_out.go diff --git a/refilter-domain/go.mod b/refilter-domain/go.mod new file mode 100644 index 0000000..e57ef79 --- /dev/null +++ b/refilter-domain/go.mod @@ -0,0 +1,14 @@ +module github.com/v2fly/domain-list-community + +go 1.19 + +require ( + github.com/v2fly/v2ray-core/v5 v5.15.1 + google.golang.org/protobuf v1.33.0 +) + +require ( + github.com/adrg/xdg v0.4.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + golang.org/x/sys v0.18.0 // indirect +) diff --git a/refilter-domain/go.sum b/refilter-domain/go.sum new file mode 100644 index 0000000..6b1b8db --- /dev/null +++ b/refilter-domain/go.sum @@ -0,0 +1,22 @@ +github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= +github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= +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/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/v2fly/v2ray-core/v5 v5.15.1 h1:n5Xvv3RkDcBYtHDpS6rkrOAS2WURfhZWSfvkxsgTK9M= +github.com/v2fly/v2ray-core/v5 v5.15.1/go.mod h1:IMP4oFah0SjutOQ3MTWmTz2LZeIielqE/sBhdAnhfd0= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/refilter-domain/main.go b/refilter-domain/main.go new file mode 100644 index 0000000..d874270 --- /dev/null +++ b/refilter-domain/main.go @@ -0,0 +1,391 @@ +package main + +import ( + "bufio" + "errors" + "flag" + "fmt" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + + router "github.com/v2fly/v2ray-core/v5/app/router/routercommon" + "google.golang.org/protobuf/proto" +) + +var ( + dataPath = flag.String("datapath", "./data", "Path to your custom 'data' directory") + outputName = flag.String("outputname", "dlc.dat", "Name of the generated dat file") + outputDir = flag.String("outputdir", "./", "Directory to place all generated files") + exportLists = flag.String("exportlists", "", "Lists to be flattened and exported in plaintext format, separated by ',' comma") +) + +type Entry struct { + Type string + Value string + Attrs []*router.Domain_Attribute +} + +type List struct { + Name string + Entry []Entry +} + +type ParsedList struct { + Name string + Inclusion map[string]bool + Entry []Entry +} + +func (l *ParsedList) toPlainText(listName string) error { + var entryBytes []byte + for _, entry := range l.Entry { + var attrString string + if entry.Attrs != nil { + for _, attr := range entry.Attrs { + attrString += "@" + attr.GetKey() + "," + } + attrString = strings.TrimRight(":"+attrString, ",") + } + // Entry output format is: type:domain.tld:@attr1,@attr2 + entryBytes = append(entryBytes, []byte(entry.Type+":"+entry.Value+attrString+"\n")...) + } + if err := os.WriteFile(filepath.Join(*outputDir, listName+".txt"), entryBytes, 0644); err != nil { + return fmt.Errorf(err.Error()) + } + return nil +} + +func (l *ParsedList) toProto() (*router.GeoSite, error) { + site := &router.GeoSite{ + CountryCode: l.Name, + } + for _, entry := range l.Entry { + switch entry.Type { + case "domain": + site.Domain = append(site.Domain, &router.Domain{ + Type: router.Domain_RootDomain, + Value: entry.Value, + Attribute: entry.Attrs, + }) + case "regexp": + site.Domain = append(site.Domain, &router.Domain{ + Type: router.Domain_Regex, + Value: entry.Value, + Attribute: entry.Attrs, + }) + case "keyword": + site.Domain = append(site.Domain, &router.Domain{ + Type: router.Domain_Plain, + Value: entry.Value, + Attribute: entry.Attrs, + }) + case "full": + site.Domain = append(site.Domain, &router.Domain{ + Type: router.Domain_Full, + Value: entry.Value, + Attribute: entry.Attrs, + }) + default: + return nil, errors.New("unknown domain type: " + entry.Type) + } + } + return site, nil +} + +func exportPlainTextList(list []string, refName string, pl *ParsedList) { + for _, listName := range list { + if strings.EqualFold(refName, listName) { + if err := pl.toPlainText(strings.ToLower(refName)); err != nil { + fmt.Println("Failed: ", err) + continue + } + fmt.Printf("'%s' has been generated successfully.\n", listName) + } + } +} + +func removeComment(line string) string { + idx := strings.Index(line, "#") + if idx == -1 { + return line + } + return strings.TrimSpace(line[:idx]) +} + +func parseDomain(domain string, entry *Entry) error { + kv := strings.Split(domain, ":") + if len(kv) == 1 { + entry.Type = "domain" + entry.Value = strings.ToLower(kv[0]) + return nil + } + + if len(kv) == 2 { + entry.Type = strings.ToLower(kv[0]) + entry.Value = strings.ToLower(kv[1]) + return nil + } + + return errors.New("Invalid format: " + domain) +} + +func parseAttribute(attr string) (*router.Domain_Attribute, error) { + var attribute router.Domain_Attribute + if len(attr) == 0 || attr[0] != '@' { + return &attribute, errors.New("invalid attribute: " + attr) + } + + // Trim attribute prefix `@` character + attr = attr[1:] + parts := strings.Split(attr, "=") + if len(parts) == 1 { + attribute.Key = strings.ToLower(parts[0]) + attribute.TypedValue = &router.Domain_Attribute_BoolValue{BoolValue: true} + } else { + attribute.Key = strings.ToLower(parts[0]) + intv, err := strconv.Atoi(parts[1]) + if err != nil { + return &attribute, errors.New("invalid attribute: " + attr + ": " + err.Error()) + } + attribute.TypedValue = &router.Domain_Attribute_IntValue{IntValue: int64(intv)} + } + return &attribute, nil +} + +func parseEntry(line string) (Entry, error) { + line = strings.TrimSpace(line) + parts := strings.Split(line, " ") + + var entry Entry + if len(parts) == 0 { + return entry, errors.New("empty entry") + } + + if err := parseDomain(parts[0], &entry); err != nil { + return entry, err + } + + for i := 1; i < len(parts); i++ { + attr, err := parseAttribute(parts[i]) + if err != nil { + return entry, err + } + entry.Attrs = append(entry.Attrs, attr) + } + + return entry, nil +} + +func Load(path string) (*List, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + + list := &List{ + Name: strings.ToUpper(filepath.Base(path)), + } + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + line = removeComment(line) + if len(line) == 0 { + continue + } + entry, err := parseEntry(line) + if err != nil { + return nil, err + } + list.Entry = append(list.Entry, entry) + } + + return list, nil +} + +func isMatchAttr(Attrs []*router.Domain_Attribute, includeKey string) bool { + isMatch := false + mustMatch := true + matchName := includeKey + if strings.HasPrefix(includeKey, "!") { + isMatch = true + mustMatch = false + matchName = strings.TrimLeft(includeKey, "!") + } + + for _, Attr := range Attrs { + attrName := Attr.Key + if mustMatch { + if matchName == attrName { + isMatch = true + break + } + } else { + if matchName == attrName { + isMatch = false + break + } + } + } + return isMatch +} + +func createIncludeAttrEntrys(list *List, matchAttr *router.Domain_Attribute) []Entry { + newEntryList := make([]Entry, 0, len(list.Entry)) + matchName := matchAttr.Key + for _, entry := range list.Entry { + matched := isMatchAttr(entry.Attrs, matchName) + if matched { + newEntryList = append(newEntryList, entry) + } + } + return newEntryList +} + +func ParseList(list *List, ref map[string]*List) (*ParsedList, error) { + pl := &ParsedList{ + Name: list.Name, + Inclusion: make(map[string]bool), + } + entryList := list.Entry + for { + newEntryList := make([]Entry, 0, len(entryList)) + hasInclude := false + for _, entry := range entryList { + if entry.Type == "include" { + refName := strings.ToUpper(entry.Value) + if entry.Attrs != nil { + for _, attr := range entry.Attrs { + InclusionName := strings.ToUpper(refName + "@" + attr.Key) + if pl.Inclusion[InclusionName] { + continue + } + pl.Inclusion[InclusionName] = true + + refList := ref[refName] + if refList == nil { + return nil, errors.New(entry.Value + " not found.") + } + attrEntrys := createIncludeAttrEntrys(refList, attr) + if len(attrEntrys) != 0 { + newEntryList = append(newEntryList, attrEntrys...) + } + } + } else { + InclusionName := refName + if pl.Inclusion[InclusionName] { + continue + } + pl.Inclusion[InclusionName] = true + refList := ref[refName] + if refList == nil { + return nil, errors.New(entry.Value + " not found.") + } + newEntryList = append(newEntryList, refList.Entry...) + } + hasInclude = true + } else { + newEntryList = append(newEntryList, entry) + } + } + entryList = newEntryList + if !hasInclude { + break + } + } + pl.Entry = entryList + + return pl, nil +} + +func main() { + flag.Parse() + + dir := *dataPath + fmt.Println("Use domain lists in", dir) + + ref := make(map[string]*List) + err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + list, err := Load(path) + if err != nil { + return err + } + ref[list.Name] = list + return nil + }) + if err != nil { + fmt.Println("Failed: ", err) + os.Exit(1) + } + + // Create output directory if not exist + if _, err := os.Stat(*outputDir); os.IsNotExist(err) { + if mkErr := os.MkdirAll(*outputDir, 0755); mkErr != nil { + fmt.Println("Failed: ", mkErr) + os.Exit(1) + } + } + + protoList := new(router.GeoSiteList) + var existList []string + for refName, list := range ref { + pl, err := ParseList(list, ref) + if err != nil { + fmt.Println("Failed: ", err) + os.Exit(1) + } + site, err := pl.toProto() + if err != nil { + fmt.Println("Failed: ", err) + os.Exit(1) + } + protoList.Entry = append(protoList.Entry, site) + + // Flatten and export plaintext list + if *exportLists != "" { + if existList != nil { + exportPlainTextList(existList, refName, pl) + } else { + exportedListSlice := strings.Split(*exportLists, ",") + for _, exportedListName := range exportedListSlice { + fileName := filepath.Join(dir, exportedListName) + _, err := os.Stat(fileName) + if err == nil || os.IsExist(err) { + existList = append(existList, exportedListName) + } else { + fmt.Printf("'%s' list does not exist in '%s' directory.\n", exportedListName, dir) + } + } + if existList != nil { + exportPlainTextList(existList, refName, pl) + } + } + } + } + + // Sort protoList so the marshaled list is reproducible + sort.SliceStable(protoList.Entry, func(i, j int) bool { + return protoList.Entry[i].CountryCode < protoList.Entry[j].CountryCode + }) + + protoBytes, err := proto.Marshal(protoList) + if err != nil { + fmt.Println("Failed:", err) + os.Exit(1) + } + if err := os.WriteFile(filepath.Join(*outputDir, *outputName), protoBytes, 0644); err != nil { + fmt.Println("Failed: ", err) + os.Exit(1) + } else { + fmt.Println(*outputName, "has been generated successfully.") + } +} diff --git a/refilter-geoip/config-example.json b/refilter-geoip/config-example.json new file mode 100644 index 0000000..8313a88 --- /dev/null +++ b/refilter-geoip/config-example.json @@ -0,0 +1,146 @@ +{ + "input": [ + { + "type": "v2rayGeoIPDat", + "action": "add", + "args": { + "uri": "./geoip.dat", + "wantedList": ["cn", "us"], + "onlyIPType": "ipv4" + } + }, + { + "type": "v2rayGeoIPDat", + "action": "add", + "args": { + "uri": "https://raw.githubusercontent.com/v2fly/geoip/release/geoip.dat", + "onlyIPType": "ipv6" + } + }, + { + "type": "maxmindGeoLite2CountryCSV", + "action": "add", + "args": { + "country": "./geolite2/GeoLite2-Country-Locations-en.csv", + "ipv4": "./geolite2/GeoLite2-Country-Blocks-IPv4.csv", + "ipv6": "./geolite2/GeoLite2-Country-Blocks-IPv6.csv", + "wantedList": ["cn", "us"] + } + }, + { + "type": "maxmindMMDB", + "action": "add" + }, + { + "type": "maxmindMMDB", + "action": "add", + "args": { + "uri": "./path/to/your/mmdb/file", + "wantedList": ["cn", "us"], + "onlyIPType": "ipv4" + } + }, + { + "type": "maxmindMMDB", + "action": "add", + "args": { + "uri": "https://example.com/link/to/your/mmdb/file" + } + }, + { + "type": "maxmindMMDB", + "action": "remove", + "args": { + "uri": "https://example.com/link/to/your/mmdb/file", + "wantedList": ["jp", "sg"], + "onlyIPType": "ipv6" + } + }, + { + "type": "cutter", + "action": "remove", + "args": { + "wantedList": ["cn", "us"], + "onlyIPType": "ipv4" + } + }, + { + "type": "cutter", + "action": "remove", + "args": { + "onlyIPType": "ipv6" + } + }, + { + "type": "text", + "action": "add", + "args": { + "inputDir": "./data" + } + }, + { + "type": "text", + "action": "add", + "args": { + "name": "mylist", + "uri": "./an/example/dir/mycidr.txt", + "onlyIPType": "ipv6" + } + }, + { + "type": "text", + "action": "add", + "args": { + "name": "cn", + "uri": "https://raw.githubusercontent.com/17mon/china_ip_list/master/china_ip_list.txt", + "onlyIPType": "ipv4" + } + }, + { + "type": "private", + "action": "add" + }, + { + "type": "test", + "action": "add" + } + ], + "output": [ + { + "type": "v2rayGeoIPDat", + "action": "output", + "args": { + "outputName": "geoip.dat", + "outputDir": "./publish" + } + }, + { + "type": "v2rayGeoIPDat", + "action": "output", + "args": { + "outputName": "geoip-only-cn-private.dat", + "outputDir": "./publish", + "wantedList": ["cn", "private"] + } + }, + { + "type": "v2rayGeoIPDat", + "action": "output", + "args": { + "outputDir": "./publish", + "oneFilePerList": true, + "wantedList": ["cn", "private", "test"], + "onlyIPType": "ipv4" + } + }, + { + "type": "text", + "action": "output", + "args": { + "outputDir": "./publish", + "wantedList": ["cn", "us", "private"], + "onlyIPType": "ipv6" + } + } + ] +} diff --git a/refilter-geoip/config.json b/refilter-geoip/config.json new file mode 100644 index 0000000..0d69df9 --- /dev/null +++ b/refilter-geoip/config.json @@ -0,0 +1,27 @@ +{ + "input": [ + { + "type": "text", + "action": "add", + "args": { + "name": "refilter", + "uri": "https://raw.githubusercontent.com/1andrevich/Re-filter-lists/refs/heads/main/ipsum.lst", + "onlyIPType": "ipv4" + } + }, + { + "type": "private", + "action": "add" + } + ], + "output": [ + { + "type": "v2rayGeoIPDat", + "action": "output", + "args": { + "outputName": "geoip.dat", + "wantedList": ["refilter", "private"] + } + } + ] +} diff --git a/refilter-geoip/go.mod b/refilter-geoip/go.mod new file mode 100644 index 0000000..8254451 --- /dev/null +++ b/refilter-geoip/go.mod @@ -0,0 +1,28 @@ +module github.com/v2fly/geoip + +go 1.19 + +require ( + github.com/oschwald/maxminddb-golang v1.10.0 + github.com/v2fly/v2ray-core/v5 v5.4.1 + go4.org/netipx v0.0.0-20230303233057-f1b76eb4bb35 + google.golang.org/protobuf v1.30.0 +) + +require ( + github.com/adrg/xdg v0.4.0 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pires/go-proxyproto v0.6.2 // indirect + github.com/quic-go/qtls-go1-18 v0.2.0 // indirect + github.com/quic-go/qtls-go1-19 v0.2.1 // indirect + github.com/quic-go/qtls-go1-20 v0.1.1 // indirect + github.com/quic-go/quic-go v0.33.0 // indirect + go.starlark.net v0.0.0-20220817180228-f738f5508c12 // indirect + golang.org/x/crypto v0.14.0 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/refilter-geoip/go.sum b/refilter-geoip/go.sum new file mode 100644 index 0000000..0ad6acd --- /dev/null +++ b/refilter-geoip/go.sum @@ -0,0 +1,175 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= +github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= +github.com/aead/cmac v0.0.0-20160719120800-7af84192f0b1 h1:+JkXLHME8vLJafGhOH4aoV2Iu8bR55nU6iKMVfYVLjY= +github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= +github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= +github.com/boljen/go-bitmap v0.0.0-20151001105940-23cd2fb0ce7d h1:zsO4lp+bjv5XvPTF58Vq+qgmZEYZttJK+CWtSZhKenI= +github.com/bufbuild/protocompile v0.2.1-0.20230123224550-da57cd758c2f h1:IXSA5gow10s7zIOJfPOpXDtNBWCTA0715BDAhoJBXEs= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165 h1:BS21ZUJ/B5X2UVUbczfmdWH7GapPWAhxcMsDnjJTU1E= +github.com/ebfe/bcrypt_pbkdf v0.0.0-20140212075826-3c8d2dcb253a h1:YtdtTUN1iH97s+6PUjLnaiKSQj4oG1/EZ3N9bx6g4kU= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0= +github.com/go-chi/render v1.0.2 h1:4ER/udB0+fMWB2Jlf15RV3F4A2FDuYi/9f+lFttR/Lg= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/jhump/protoreflect v1.15.0 h1:U5T5/2LF0AZQFP9T4W5GfBjBaTruomrKobiR4E+oA/Q= +github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw= +github.com/klauspost/cpuid v1.2.3 h1:CCtW0xUnWGVINKvE/WWOYKdsPV6mawAtvQuSl8guwQs= +github.com/klauspost/reedsolomon v1.9.3 h1:N/VzgeMfHmLc+KHMD1UL/tNkfXAt8FnUqlgXGIduwAY= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc= +github.com/mustafaturan/bus v1.0.2 h1:2x3ErwZ0uUPwwZ5ZZoknEQprdaxr68Yl3mY8jDye1Ws= +github.com/mustafaturan/monoton v1.0.0 h1:8SCej+JiNn0lyps7V+Jzc1CRAkDR4EZPWrTupQ61YCQ= +github.com/onsi/ginkgo/v2 v2.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI= +github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q= +github.com/oschwald/maxminddb-golang v1.10.0 h1:Xp1u0ZhqkSuopaKmk1WwHtjF0H9Hd9181uj2MQ5Vndg= +github.com/oschwald/maxminddb-golang v1.10.0/go.mod h1:Y2ELenReaLAZ0b400URyGwvYxHV1dLIxBuyOsyYjHK0= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pion/dtls/v2 v2.2.4 h1:YSfYwDQgrxMYXLBc/m7PFY5BVtWlNm/DN4qoU2CbcWg= +github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= +github.com/pion/sctp v1.7.6 h1:8qZTdJtbKfAns/Hv5L0PAj8FyXcsKhMH1pKUCGisQg4= +github.com/pion/transport/v2 v2.0.0 h1:bsMYyqHCbkvHwj+eNCFBuxtlKndKfyGI2vaQmM3fIE4= +github.com/pion/udp v0.1.4 h1:OowsTmu1Od3sD6i3fQUJxJn2fEvJO6L1TidgadtbTI8= +github.com/pires/go-proxyproto v0.6.2 h1:KAZ7UteSOt6urjme6ZldyFm4wDe/z0ZUP0Yv0Dos0d8= +github.com/pires/go-proxyproto v0.6.2/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +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_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/quic-go/qtls-go1-18 v0.2.0 h1:5ViXqBZ90wpUcZS0ge79rf029yx0dYB0McyPJwqqj7U= +github.com/quic-go/qtls-go1-18 v0.2.0/go.mod h1:moGulGHK7o6O8lSPSZNoOwcLvJKJ85vVNc7oJFD65bc= +github.com/quic-go/qtls-go1-19 v0.2.1 h1:aJcKNMkH5ASEJB9FXNeZCyTEIHU1J7MmHyz1Q1TSG1A= +github.com/quic-go/qtls-go1-19 v0.2.1/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI= +github.com/quic-go/qtls-go1-20 v0.1.1 h1:KbChDlg82d3IHqaj2bn6GfKRj84Per2VGf5XV3wSwQk= +github.com/quic-go/qtls-go1-20 v0.1.1/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM= +github.com/quic-go/quic-go v0.33.0 h1:ItNoTDN/Fm/zBlq769lLJc8ECe9gYaW40veHCCco7y0= +github.com/quic-go/quic-go v0.33.0/go.mod h1:YMuhaAV9/jIu0XclDXwZPAsP/2Kgr5yMYhe9oxhhOFA= +github.com/refraction-networking/utls v1.3.1 h1:3zVomUqx7nCmyGuU/6kYA/jp5NcqX8KQSGko8pY5Ch4= +github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/secure-io/siv-go v0.0.0-20180922214919-5ff40651e2c4 h1:zOjq+1/uLzn/Xo40stbvjIY/yehG0+mfmlsiEmc0xmQ= +github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/v2fly/BrowserBridge v0.0.0-20210430233438-0570fc1d7d08 h1:4Yh46CVE3k/lPq6hUbEdbB1u1anRBXLewm3k+L0iOMc= +github.com/v2fly/VSign v0.0.0-20201108000810-e2adc24bf848 h1:p1UzXK6VAutXFFQMnre66h7g1BjRKUnLv0HfmmRoz7w= +github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI= +github.com/v2fly/v2ray-core/v5 v5.4.1 h1:1l3KIFKoOlZkUp6D9MUlN/gz6aOEx09YfRVmnwnoGIQ= +github.com/v2fly/v2ray-core/v5 v5.4.1/go.mod h1:8JUFMS/1biOF9rWV7V5IU3NKU8GhCd442MqW3DgdFKw= +github.com/xiaokangwang/VLite v0.0.0-20220418190619-cff95160a432 h1:I/ATawgO2RerCq9ACwL0wBB8xNXZdE3J+93MCEHReRs= +github.com/xtaci/smux v1.5.15 h1:6hMiXswcleXj5oNfcJc+DXS8Vj36XX2LaX98udog6Kc= +go.starlark.net v0.0.0-20220817180228-f738f5508c12 h1:xOBJXWGEDwU5xSDxH6macxO11Us0AH2fTa9rmsbbF7g= +go.starlark.net v0.0.0-20220817180228-f738f5508c12/go.mod h1:VZcBMdr3cT3PnBoWunTabuSEXwVAH+ZJ5zxfs3AdASk= +go4.org/netipx v0.0.0-20230303233057-f1b76eb4bb35 h1:nJAwRlGWZZDOD+6wni9KVUNHMpHko/OnRwsrCYeAzPo= +go4.org/netipx v0.0.0-20230303233057-f1b76eb4bb35/go.mod h1:TQvodOM+hJTioNQJilmLXu08JNb8i+ccq418+KWu1/Y= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/refilter-geoip/init.go b/refilter-geoip/init.go new file mode 100644 index 0000000..c137de8 --- /dev/null +++ b/refilter-geoip/init.go @@ -0,0 +1,8 @@ +package main + +import ( + _ "github.com/v2fly/geoip/plugin/maxmind" + _ "github.com/v2fly/geoip/plugin/plaintext" + _ "github.com/v2fly/geoip/plugin/special" + _ "github.com/v2fly/geoip/plugin/v2ray" +) diff --git a/refilter-geoip/lib/config.go b/refilter-geoip/lib/config.go new file mode 100644 index 0000000..20e5137 --- /dev/null +++ b/refilter-geoip/lib/config.go @@ -0,0 +1,128 @@ +package lib + +import ( + "encoding/json" + "errors" + "fmt" + "strings" +) + +var ( + inputConfigCreatorCache = make(map[string]inputConfigCreator) + outputConfigCreatorCache = make(map[string]outputConfigCreator) +) + +type inputConfigCreator func(Action, json.RawMessage) (InputConverter, error) + +type outputConfigCreator func(Action, json.RawMessage) (OutputConverter, error) + +func RegisterInputConfigCreator(id string, fn inputConfigCreator) error { + id = strings.ToLower(id) + if _, found := inputConfigCreatorCache[id]; found { + return errors.New("config creator has already been registered") + } + inputConfigCreatorCache[id] = fn + return nil +} + +func createInputConfig(id string, action Action, data json.RawMessage) (InputConverter, error) { + id = strings.ToLower(id) + fn, found := inputConfigCreatorCache[id] + if !found { + return nil, errors.New("unknown config type") + } + return fn(action, data) +} + +func RegisterOutputConfigCreator(id string, fn outputConfigCreator) error { + id = strings.ToLower(id) + if _, found := outputConfigCreatorCache[id]; found { + return errors.New("config creator has already been registered") + } + outputConfigCreatorCache[id] = fn + return nil +} + +func createOutputConfig(id string, action Action, data json.RawMessage) (OutputConverter, error) { + id = strings.ToLower(id) + fn, found := outputConfigCreatorCache[id] + if !found { + return nil, errors.New("unknown config type") + } + return fn(action, data) +} + +type config struct { + Input []*inputConvConfig `json:"input"` + Output []*outputConvConfig `json:"output"` +} + +type inputConvConfig struct { + iType string + action Action + converter InputConverter +} + +func (i *inputConvConfig) UnmarshalJSON(data []byte) error { + var temp struct { + Type string `json:"type"` + Action Action `json:"action"` + Args json.RawMessage `json:"args"` + } + + if err := json.Unmarshal(data, &temp); err != nil { + return err + } + + if !ActionsRegistry[temp.Action] { + return fmt.Errorf("invalid action %s in type %s", temp.Action, temp.Type) + } + + config, err := createInputConfig(temp.Type, temp.Action, temp.Args) + if err != nil { + return err + } + + i.iType = config.GetType() + i.action = config.GetAction() + i.converter = config + + return nil +} + +type outputConvConfig struct { + iType string + action Action + converter OutputConverter +} + +func (i *outputConvConfig) UnmarshalJSON(data []byte) error { + var temp struct { + Type string `json:"type"` + Action Action `json:"action"` + Args json.RawMessage `json:"args"` + } + + if err := json.Unmarshal(data, &temp); err != nil { + return err + } + + if temp.Action == "" { + temp.Action = ActionOutput + } + + if !ActionsRegistry[temp.Action] { + return fmt.Errorf("invalid action %s in type %s", temp.Action, temp.Type) + } + + config, err := createOutputConfig(temp.Type, temp.Action, temp.Args) + if err != nil { + return err + } + + i.iType = config.GetType() + i.action = config.GetAction() + i.converter = config + + return nil +} diff --git a/refilter-geoip/lib/error.go b/refilter-geoip/lib/error.go new file mode 100644 index 0000000..5778e96 --- /dev/null +++ b/refilter-geoip/lib/error.go @@ -0,0 +1,13 @@ +package lib + +import "errors" + +var ( + ErrDuplicatedConverter = errors.New("duplicated converter") + ErrUnknownAction = errors.New("unknown action") + ErrInvalidIPType = errors.New("invalid IP type") + ErrInvalidIP = errors.New("invalid IP address") + ErrInvalidIPLength = errors.New("invalid IP address length") + ErrInvalidIPNet = errors.New("invalid IPNet address") + ErrInvalidPrefixType = errors.New("invalid prefix type") +) diff --git a/refilter-geoip/lib/func.go b/refilter-geoip/lib/func.go new file mode 100644 index 0000000..8cef3ce --- /dev/null +++ b/refilter-geoip/lib/func.go @@ -0,0 +1,43 @@ +package lib + +import ( + "fmt" + "strings" +) + +var ( + inputConverterMap = make(map[string]InputConverter) + outputConverterMap = make(map[string]OutputConverter) +) + +func ListInputConverter() { + fmt.Println("All available input formats:") + for name, ic := range inputConverterMap { + fmt.Printf(" - %s (%s)\n", name, ic.GetDescription()) + } +} + +func RegisterInputConverter(name string, c InputConverter) error { + name = strings.TrimSpace(name) + if _, ok := inputConverterMap[name]; ok { + return ErrDuplicatedConverter + } + inputConverterMap[name] = c + return nil +} + +func ListOutputConverter() { + fmt.Println("All available output formats:") + for name, oc := range outputConverterMap { + fmt.Printf(" - %s (%s)\n", name, oc.GetDescription()) + } +} + +func RegisterOutputConverter(name string, c OutputConverter) error { + name = strings.TrimSpace(name) + if _, ok := outputConverterMap[name]; ok { + return ErrDuplicatedConverter + } + outputConverterMap[name] = c + return nil +} diff --git a/refilter-geoip/lib/instance.go b/refilter-geoip/lib/instance.go new file mode 100644 index 0000000..5631766 --- /dev/null +++ b/refilter-geoip/lib/instance.go @@ -0,0 +1,65 @@ +package lib + +import ( + "encoding/json" + "errors" + "os" +) + +type Instance struct { + config *config + input []InputConverter + output []OutputConverter +} + +func NewInstance() (*Instance, error) { + return &Instance{ + config: new(config), + input: make([]InputConverter, 0), + output: make([]OutputConverter, 0), + }, nil +} + +func (i *Instance) Init(configFile string) error { + content, err := os.ReadFile(configFile) + if err != nil { + return err + } + + if err := json.Unmarshal(content, &i.config); err != nil { + return err + } + + for _, input := range i.config.Input { + i.input = append(i.input, input.converter) + } + + for _, output := range i.config.Output { + i.output = append(i.output, output.converter) + } + + return nil +} + +func (i *Instance) Run() error { + if len(i.input) == 0 || len(i.output) == 0 { + return errors.New("input type and output type must be specified") + } + + var err error + container := NewContainer() + for _, ic := range i.input { + container, err = ic.Input(container) + if err != nil { + return err + } + } + + for _, oc := range i.output { + if err := oc.Output(container); err != nil { + return err + } + } + + return nil +} diff --git a/refilter-geoip/lib/lib.go b/refilter-geoip/lib/lib.go new file mode 100644 index 0000000..d1d23ab --- /dev/null +++ b/refilter-geoip/lib/lib.go @@ -0,0 +1,458 @@ +package lib + +import ( + "fmt" + "log" + "net" + "net/netip" + "strings" + "sync" + + "go4.org/netipx" +) + +const ( + ActionAdd Action = "add" + ActionRemove Action = "remove" + ActionOutput Action = "output" + + IPv4 IPType = "ipv4" + IPv6 IPType = "ipv6" +) + +var ActionsRegistry = map[Action]bool{ + ActionAdd: true, + ActionRemove: true, + ActionOutput: true, +} + +type Action string + +type IPType string + +type Typer interface { + GetType() string +} + +type Actioner interface { + GetAction() Action +} + +type Descriptioner interface { + GetDescription() string +} + +type InputConverter interface { + Typer + Actioner + Descriptioner + Input(Container) (Container, error) +} + +type OutputConverter interface { + Typer + Actioner + Descriptioner + Output(Container) error +} + +type Entry struct { + name string + mu *sync.Mutex + ipv4Builder *netipx.IPSetBuilder + ipv6Builder *netipx.IPSetBuilder +} + +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), + } +} + +func (e *Entry) GetName() string { + return e.name +} + +func (e *Entry) hasIPv4Builder() bool { + return e.ipv4Builder != nil +} + +func (e *Entry) hasIPv6Builder() bool { + return e.ipv6Builder != nil +} + +func (e *Entry) processPrefix(src any) (*netip.Prefix, IPType, error) { + switch src := src.(type) { + case net.IP: + ip, ok := netipx.FromStdIP(src) + if !ok { + return nil, "", ErrInvalidIP + } + switch { + case ip.Is4(): + prefix := netip.PrefixFrom(ip, 32) + return &prefix, IPv4, nil + case ip.Is6(): + prefix := netip.PrefixFrom(ip, 128) + return &prefix, IPv6, nil + default: + return nil, "", ErrInvalidIPLength + } + + case *net.IPNet: + prefix, ok := netipx.FromStdIPNet(src) + if !ok { + return nil, "", ErrInvalidIPNet + } + ip := prefix.Addr() + switch { + case ip.Is4(): + return &prefix, IPv4, nil + case ip.Is6(): + return &prefix, IPv6, nil + default: + return nil, "", ErrInvalidIPLength + } + + case netip.Addr: + switch { + case src.Is4(): + prefix := netip.PrefixFrom(src, 32) + return &prefix, IPv4, nil + case src.Is6(): + prefix := netip.PrefixFrom(src, 128) + return &prefix, IPv6, nil + default: + return nil, "", ErrInvalidIPLength + } + + case *netip.Addr: + switch { + case src.Is4(): + prefix := netip.PrefixFrom(*src, 32) + return &prefix, IPv4, nil + case src.Is6(): + prefix := netip.PrefixFrom(*src, 128) + return &prefix, IPv6, nil + default: + return nil, "", ErrInvalidIPLength + } + + case netip.Prefix: + ip := src.Addr() + switch { + case ip.Is4(): + return &src, IPv4, nil + case ip.Is6(): + return &src, IPv6, nil + default: + return nil, "", ErrInvalidIPLength + } + + case *netip.Prefix: + ip := src.Addr() + switch { + case ip.Is4(): + return src, IPv4, nil + case ip.Is6(): + return src, IPv6, nil + default: + return nil, "", ErrInvalidIPLength + } + + case string: + _, network, err := net.ParseCIDR(src) + switch err { + case nil: + prefix, ok := netipx.FromStdIPNet(network) + if !ok { + return nil, "", ErrInvalidIPNet + } + ip := prefix.Addr() + switch { + case ip.Is4(): + return &prefix, IPv4, nil + case ip.Is6(): + return &prefix, IPv6, nil + default: + return nil, "", ErrInvalidIPLength + } + + default: + ip, err := netip.ParseAddr(src) + if err != nil { + return nil, "", err + } + switch { + case ip.Is4(): + prefix := netip.PrefixFrom(ip, 32) + return &prefix, IPv4, nil + case ip.Is6(): + prefix := netip.PrefixFrom(ip, 128) + return &prefix, IPv6, nil + default: + return nil, "", ErrInvalidIPLength + } + } + } + + return nil, "", ErrInvalidPrefixType +} + +func (e *Entry) add(prefix *netip.Prefix, ipType IPType) error { + e.mu.Lock() + defer e.mu.Unlock() + + switch ipType { + case IPv4: + if !e.hasIPv4Builder() { + e.ipv4Builder = new(netipx.IPSetBuilder) + } + e.ipv4Builder.AddPrefix(*prefix) + case IPv6: + if !e.hasIPv6Builder() { + e.ipv6Builder = new(netipx.IPSetBuilder) + } + e.ipv6Builder.AddPrefix(*prefix) + default: + return ErrInvalidIPType + } + + return nil +} + +func (e *Entry) remove(prefix *netip.Prefix, ipType IPType) error { + e.mu.Lock() + defer e.mu.Unlock() + + switch ipType { + case IPv4: + if e.hasIPv4Builder() { + e.ipv4Builder.RemovePrefix(*prefix) + } + case IPv6: + if e.hasIPv6Builder() { + e.ipv6Builder.RemovePrefix(*prefix) + } + default: + return ErrInvalidIPType + } + + return nil +} + +func (e *Entry) AddPrefix(cidr any) error { + prefix, ipType, err := e.processPrefix(cidr) + if err != nil { + return err + } + if err := e.add(prefix, ipType); err != nil { + return err + } + return nil +} + +func (e *Entry) RemovePrefix(cidr string) error { + prefix, ipType, err := e.processPrefix(cidr) + if err != nil { + return err + } + if err := e.remove(prefix, ipType); err != nil { + return err + } + return nil +} + +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 + } + + prefixSet := make([]string, 0, 1024) + + 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()) + } + } + + if !disableIPv6 && e.hasIPv6Builder() { + ipv6set, err := e.ipv6Builder.IPSet() + if err != nil { + return nil, err + } + prefixes := ipv6set.Prefixes() + for _, prefix := range prefixes { + prefixSet = append(prefixSet, prefix.String()) + } + } + + if len(prefixSet) > 0 { + return prefixSet, nil + } + + return nil, fmt.Errorf("entry %s has no prefix", e.GetName()) +} + +type IgnoreIPOption func() IPType + +func IgnoreIPv4() IPType { + return IPv4 +} + +func IgnoreIPv6() IPType { + return IPv6 +} + +type Container interface { + GetEntry(name string) (*Entry, bool) + Add(entry *Entry, opts ...IgnoreIPOption) error + Remove(name string, opts ...IgnoreIPOption) + Loop() <-chan *Entry +} + +type container struct { + entries *sync.Map // map[name]*Entry +} + +func NewContainer() Container { + return &container{ + entries: new(sync.Map), + } +} + +func (c *container) isValid() bool { + if c == nil || c.entries == nil { + return false + } + return true +} + +func (c *container) GetEntry(name string) (*Entry, bool) { + if !c.isValid() { + return nil, false + } + val, ok := c.entries.Load(strings.ToUpper(strings.TrimSpace(name))) + if !ok { + return nil, false + } + return val.(*Entry), true +} + +func (c *container) Loop() <-chan *Entry { + ch := make(chan *Entry, 300) + go func() { + c.entries.Range(func(key, value any) bool { + ch <- value.(*Entry) + return true + }) + close(ch) + }() + return ch +} + +func (c *container) Add(entry *Entry, opts ...IgnoreIPOption) error { + var ignoreIPType IPType + for _, opt := range opts { + if opt != nil { + ignoreIPType = opt() + } + } + + name := entry.GetName() + val, found := c.GetEntry(name) + switch found { + case true: + var ipv4set, ipv6set *netipx.IPSet + var err4, err6 error + if entry.hasIPv4Builder() { + ipv4set, err4 = entry.ipv4Builder.IPSet() + if err4 != nil { + return err4 + } + } + if entry.hasIPv6Builder() { + ipv6set, err6 = entry.ipv6Builder.IPSet() + if err6 != nil { + return err6 + } + } + switch ignoreIPType { + case IPv4: + if !val.hasIPv6Builder() { + val.ipv6Builder = new(netipx.IPSetBuilder) + } + val.ipv6Builder.AddSet(ipv6set) + case IPv6: + if !val.hasIPv4Builder() { + val.ipv4Builder = new(netipx.IPSetBuilder) + } + val.ipv4Builder.AddSet(ipv4set) + default: + if !val.hasIPv4Builder() { + val.ipv4Builder = new(netipx.IPSetBuilder) + } + if !val.hasIPv6Builder() { + val.ipv6Builder = new(netipx.IPSetBuilder) + } + val.ipv4Builder.AddSet(ipv4set) + val.ipv6Builder.AddSet(ipv6set) + } + c.entries.Store(name, val) + + case false: + switch ignoreIPType { + case IPv4: + entry.ipv4Builder = nil + case IPv6: + entry.ipv6Builder = nil + } + c.entries.Store(name, entry) + } + + return nil +} + +func (c *container) Remove(name string, opts ...IgnoreIPOption) { + val, found := c.GetEntry(name) + if !found { + log.Printf("failed to remove non-existent entry %s", name) + return + } + + var ignoreIPType IPType + for _, opt := range opts { + if opt != nil { + ignoreIPType = opt() + } + } + + switch ignoreIPType { + case IPv4: + val.ipv6Builder = nil + c.entries.Store(name, val) + case IPv6: + val.ipv4Builder = nil + c.entries.Store(name, val) + default: + c.entries.Delete(name) + } +} diff --git a/refilter-geoip/main.go b/refilter-geoip/main.go new file mode 100644 index 0000000..c35696b --- /dev/null +++ b/refilter-geoip/main.go @@ -0,0 +1,41 @@ +// GeoIP generator +// +// Before running this file, the GeoIP database must be downloaded and present. +// To download GeoIP database: https://dev.maxmind.com/geoip/geoip2/geolite2/ +// Inside you will find block files for IPv4 and IPv6 and country code mapping. +package main + +import ( + "flag" + "log" + + "github.com/v2fly/geoip/lib" +) + +var ( + list = flag.Bool("l", false, "List all available input and output formats") + configFile = flag.String("c", "config.json", "Path to the config file") +) + +func main() { + flag.Parse() + + if *list { + lib.ListInputConverter() + lib.ListOutputConverter() + return + } + + instance, err := lib.NewInstance() + if err != nil { + log.Fatal(err) + } + + if err := instance.Init(*configFile); err != nil { + log.Fatal(err) + } + + if err := instance.Run(); err != nil { + log.Fatal(err) + } +} diff --git a/refilter-geoip/plugin/maxmind/country_csv.go b/refilter-geoip/plugin/maxmind/country_csv.go new file mode 100644 index 0000000..7773491 --- /dev/null +++ b/refilter-geoip/plugin/maxmind/country_csv.go @@ -0,0 +1,214 @@ +package maxmind + +import ( + "encoding/csv" + "encoding/json" + "errors" + "os" + "path/filepath" + "strings" + + "github.com/v2fly/geoip/lib" +) + +const ( + typeCountryCSV = "maxmindGeoLite2CountryCSV" + descCountryCSV = "Convert MaxMind GeoLite2 country CSV data to other formats" +) + +var ( + defaultCCFile = filepath.Join("./", "geolite2", "GeoLite2-Country-Locations-en.csv") + defaultIPv4File = filepath.Join("./", "geolite2", "GeoLite2-Country-Blocks-IPv4.csv") + defaultIPv6File = filepath.Join("./", "geolite2", "GeoLite2-Country-Blocks-IPv6.csv") +) + +func init() { + lib.RegisterInputConfigCreator(typeCountryCSV, func(action lib.Action, data json.RawMessage) (lib.InputConverter, error) { + return newGeoLite2CountryCSV(action, data) + }) + lib.RegisterInputConverter(typeCountryCSV, &geoLite2CountryCSV{ + Description: descCountryCSV, + }) +} + +func newGeoLite2CountryCSV(action lib.Action, data json.RawMessage) (lib.InputConverter, error) { + var tmp struct { + CountryCodeFile string `json:"country"` + IPv4File string `json:"ipv4"` + IPv6File string `json:"ipv6"` + Want []string `json:"wantedList"` + OnlyIPType lib.IPType `json:"onlyIPType"` + } + + if len(data) > 0 { + if err := json.Unmarshal(data, &tmp); err != nil { + return nil, err + } + } + + if tmp.CountryCodeFile == "" { + tmp.CountryCodeFile = defaultCCFile + } + + if tmp.IPv4File == "" { + tmp.IPv4File = defaultIPv4File + } + + if tmp.IPv6File == "" { + tmp.IPv6File = defaultIPv6File + } + + return &geoLite2CountryCSV{ + Type: typeCountryCSV, + Action: action, + Description: descCountryCSV, + CountryCodeFile: tmp.CountryCodeFile, + IPv4File: tmp.IPv4File, + IPv6File: tmp.IPv6File, + Want: tmp.Want, + OnlyIPType: tmp.OnlyIPType, + }, nil +} + +type geoLite2CountryCSV struct { + Type string + Action lib.Action + Description string + CountryCodeFile string + IPv4File string + IPv6File string + Want []string + OnlyIPType lib.IPType +} + +func (g *geoLite2CountryCSV) GetType() string { + return g.Type +} + +func (g *geoLite2CountryCSV) GetAction() lib.Action { + return g.Action +} + +func (g *geoLite2CountryCSV) GetDescription() string { + return g.Description +} + +func (g *geoLite2CountryCSV) Input(container lib.Container) (lib.Container, error) { + ccMap, err := g.getCountryCode() + if err != nil { + return nil, err + } + + entries := make(map[string]*lib.Entry) + + if g.IPv4File != "" { + if err := g.process(g.IPv4File, ccMap, entries); err != nil { + return nil, err + } + } + + if g.IPv6File != "" { + if err := g.process(g.IPv6File, ccMap, entries); err != nil { + return nil, err + } + } + + var ignoreIPType lib.IgnoreIPOption + switch g.OnlyIPType { + case lib.IPv4: + ignoreIPType = lib.IgnoreIPv6 + case lib.IPv6: + ignoreIPType = lib.IgnoreIPv4 + } + + for name, entry := range entries { + switch g.Action { + case lib.ActionAdd: + if err := container.Add(entry, ignoreIPType); err != nil { + return nil, err + } + case lib.ActionRemove: + container.Remove(name, ignoreIPType) + default: + return nil, lib.ErrUnknownAction + } + } + + return container, nil +} + +func (g *geoLite2CountryCSV) getCountryCode() (map[string]string, error) { + ccReader, err := os.Open(g.CountryCodeFile) + if err != nil { + return nil, err + } + defer ccReader.Close() + + reader := csv.NewReader(ccReader) + lines, err := reader.ReadAll() + if err != nil { + return nil, err + } + + ccMap := make(map[string]string) + for _, line := range lines[1:] { + id := strings.TrimSpace(line[0]) + countryCode := strings.TrimSpace(line[4]) + if id == "" || countryCode == "" { + continue + } + ccMap[id] = strings.ToUpper(countryCode) + } + return ccMap, nil +} + +func (g *geoLite2CountryCSV) process(file string, ccMap map[string]string, entries map[string]*lib.Entry) error { + if len(ccMap) == 0 { + return errors.New("country code list must be specified") + } + if entries == nil { + entries = make(map[string]*lib.Entry) + } + + fReader, err := os.Open(file) + if err != nil { + return err + } + defer fReader.Close() + + reader := csv.NewReader(fReader) + lines, err := reader.ReadAll() + if err != nil { + return err + } + + // Filter want list + wantList := make(map[string]bool) + for _, want := range g.Want { + if want = strings.ToUpper(strings.TrimSpace(want)); want != "" { + wantList[want] = true + } + } + + for _, line := range lines[1:] { + ccID := strings.TrimSpace(line[1]) + if countryCode, found := ccMap[ccID]; found { + if len(wantList) > 0 { + if _, found := wantList[countryCode]; !found { + continue + } + } + cidrStr := strings.ToLower(strings.TrimSpace(line[0])) + entry, found := entries[countryCode] + if !found { + entry = lib.NewEntry(countryCode) + } + if err := entry.AddPrefix(cidrStr); err != nil { + return err + } + entries[countryCode] = entry + } + } + + return nil +} diff --git a/refilter-geoip/plugin/maxmind/mmdb_in.go b/refilter-geoip/plugin/maxmind/mmdb_in.go new file mode 100644 index 0000000..9261f18 --- /dev/null +++ b/refilter-geoip/plugin/maxmind/mmdb_in.go @@ -0,0 +1,227 @@ +package maxmind + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/oschwald/maxminddb-golang" + "github.com/v2fly/geoip/lib" +) + +const ( + typeMaxmindMMDBIn = "maxmindMMDB" + descMaxmindMMDBIn = "Convert MaxMind mmdb database to other formats" +) + +var ( + defaultMMDBFile = filepath.Join("./", "geolite2", "GeoLite2-Country.mmdb") + tempMMDBPath = filepath.Join("./", "tmp") + tempMMDBFile = filepath.Join(tempMMDBPath, "input.mmdb") +) + +func init() { + lib.RegisterInputConfigCreator(typeMaxmindMMDBIn, func(action lib.Action, data json.RawMessage) (lib.InputConverter, error) { + return newMaxmindMMDBIn(action, data) + }) + lib.RegisterInputConverter(typeMaxmindMMDBIn, &maxmindMMDBIn{ + Description: descMaxmindMMDBIn, + }) +} + +func newMaxmindMMDBIn(action lib.Action, data json.RawMessage) (lib.InputConverter, error) { + var tmp struct { + URI string `json:"uri"` + Want []string `json:"wantedList"` + OnlyIPType lib.IPType `json:"onlyIPType"` + } + + if len(data) > 0 { + if err := json.Unmarshal(data, &tmp); err != nil { + return nil, err + } + } + + if tmp.URI == "" { + tmp.URI = defaultMMDBFile + } + + return &maxmindMMDBIn{ + Type: typeMaxmindMMDBIn, + Action: action, + Description: descMaxmindMMDBIn, + URI: tmp.URI, + Want: tmp.Want, + OnlyIPType: tmp.OnlyIPType, + }, nil +} + +type maxmindMMDBIn struct { + Type string + Action lib.Action + Description string + URI string + Want []string + OnlyIPType lib.IPType +} + +func (g *maxmindMMDBIn) GetType() string { + return g.Type +} + +func (g *maxmindMMDBIn) GetAction() lib.Action { + return g.Action +} + +func (g *maxmindMMDBIn) GetDescription() string { + return g.Description +} + +func (g *maxmindMMDBIn) Input(container lib.Container) (lib.Container, error) { + var fd io.ReadCloser + var err error + switch { + case strings.HasPrefix(g.URI, "http://"), strings.HasPrefix(g.URI, "https://"): + fd, err = g.downloadFile(g.URI) + default: + fd, err = os.Open(g.URI) + } + + if err != nil { + return nil, err + } + + err = g.moveFile(fd) + if err != nil { + return nil, err + } + + entries := make(map[string]*lib.Entry) + err = g.generateEntries(entries) + if err != nil { + return nil, err + } + + if len(entries) == 0 { + return nil, fmt.Errorf("❌ [type %s | action %s] no entry is newly generated", typeMaxmindMMDBIn, g.Action) + } + + var ignoreIPType lib.IgnoreIPOption + switch g.OnlyIPType { + case lib.IPv4: + ignoreIPType = lib.IgnoreIPv6 + case lib.IPv6: + ignoreIPType = lib.IgnoreIPv4 + } + + // Filter want list + wantList := make(map[string]bool) + for _, want := range g.Want { + if want = strings.ToUpper(strings.TrimSpace(want)); want != "" { + wantList[want] = true + } + } + + for _, entry := range entries { + name := entry.GetName() + if len(wantList) > 0 && !wantList[name] { + continue + } + + switch g.Action { + case lib.ActionAdd: + if err := container.Add(entry, ignoreIPType); err != nil { + return nil, err + } + case lib.ActionRemove: + container.Remove(name, ignoreIPType) + } + } + + return container, nil +} + +func (g *maxmindMMDBIn) downloadFile(url string) (io.ReadCloser, error) { + resp, err := http.Get(url) + if err != nil { + return nil, err + } + + if resp.StatusCode != 200 { + return nil, fmt.Errorf("failed to get remote file %s, http status code %d", url, resp.StatusCode) + } + + return resp.Body, nil +} + +func (g *maxmindMMDBIn) moveFile(src io.ReadCloser) error { + defer src.Close() + + err := os.MkdirAll(tempMMDBPath, 0755) + if err != nil { + return err + } + + out, err := os.Create(tempMMDBFile) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, src) + + return err +} + +func (g *maxmindMMDBIn) generateEntries(entries map[string]*lib.Entry) error { + db, err := maxminddb.Open(tempMMDBFile) + if err != nil { + return err + } + defer db.Close() + + record := struct { + Country struct { + IsoCode string `maxminddb:"iso_code"` + } `maxminddb:"country"` + }{} + + networks := db.Networks(maxminddb.SkipAliasedNetworks) + for networks.Next() { + subnet, err := networks.Network(&record) + if err != nil { + continue + } + + var entry *lib.Entry + name := strings.ToUpper(record.Country.IsoCode) + if theEntry, found := entries[name]; found { + entry = theEntry + } else { + entry = lib.NewEntry(name) + } + + switch g.Action { + case lib.ActionAdd: + if err := entry.AddPrefix(subnet); err != nil { + return err + } + case lib.ActionRemove: + if err := entry.RemovePrefix(subnet.String()); err != nil { + return err + } + } + + entries[name] = entry + } + + if networks.Err() != nil { + return networks.Err() + } + + return nil +} diff --git a/refilter-geoip/plugin/plaintext/text_in.go b/refilter-geoip/plugin/plaintext/text_in.go new file mode 100644 index 0000000..f3fd7ed --- /dev/null +++ b/refilter-geoip/plugin/plaintext/text_in.go @@ -0,0 +1,227 @@ +package plaintext + +import ( + "bufio" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/v2fly/geoip/lib" +) + +const ( + typeTextIn = "text" + descTextIn = "Convert plaintext IP and CIDR to other formats" +) + +func init() { + lib.RegisterInputConfigCreator(typeTextIn, func(action lib.Action, data json.RawMessage) (lib.InputConverter, error) { + return newTextIn(action, data) + }) + lib.RegisterInputConverter(typeTextIn, &textIn{ + Description: descTextIn, + }) +} + +func newTextIn(action lib.Action, data json.RawMessage) (lib.InputConverter, error) { + var tmp struct { + Name string `json:"name"` + URI string `json:"uri"` + InputDir string `json:"inputDir"` + OnlyIPType lib.IPType `json:"onlyIPType"` + } + + if len(data) > 0 { + if err := json.Unmarshal(data, &tmp); err != nil { + return nil, err + } + } + + if tmp.Name == "" && tmp.URI == "" && tmp.InputDir == "" { + return nil, fmt.Errorf("type %s | action %s missing inputdir or name or uri", typeTextIn, action) + } + + if (tmp.Name != "" && tmp.URI == "") || (tmp.Name == "" && tmp.URI != "") { + return nil, fmt.Errorf("type %s | action %s name & uri must be specified together", typeTextIn, action) + } + + return &textIn{ + Type: typeTextIn, + Action: action, + Description: descTextIn, + Name: tmp.Name, + URI: tmp.URI, + InputDir: tmp.InputDir, + OnlyIPType: tmp.OnlyIPType, + }, nil +} + +type textIn struct { + Type string + Action lib.Action + Description string + Name string + URI string + InputDir string + OnlyIPType lib.IPType +} + +func (t *textIn) GetType() string { + return t.Type +} + +func (t *textIn) GetAction() lib.Action { + return t.Action +} + +func (t *textIn) GetDescription() string { + return t.Description +} + +func (t *textIn) Input(container lib.Container) (lib.Container, error) { + entries := make(map[string]*lib.Entry) + var err error + + switch { + case t.InputDir != "": + err = t.walkDir(t.InputDir, entries) + case t.Name != "" && t.URI != "": + switch { + case strings.HasPrefix(t.URI, "http://"), strings.HasPrefix(t.URI, "https://"): + err = t.walkRemoteFile(t.URI, t.Name, entries) + default: + err = t.walkLocalFile(t.URI, t.Name, entries) + } + default: + return nil, fmt.Errorf("config missing argument inputDir or name or uri") + } + + if err != nil { + return nil, err + } + + var ignoreIPType lib.IgnoreIPOption + switch t.OnlyIPType { + case lib.IPv4: + ignoreIPType = lib.IgnoreIPv6 + case lib.IPv6: + ignoreIPType = lib.IgnoreIPv4 + } + + if len(entries) == 0 { + return nil, fmt.Errorf("type %s | action %s no entry are generated", t.Type, t.Action) + } + + for _, entry := range entries { + switch t.Action { + case lib.ActionAdd: + if err := container.Add(entry, ignoreIPType); err != nil { + return nil, err + } + case lib.ActionRemove: + container.Remove(entry.GetName(), ignoreIPType) + } + } + + return container, nil +} + +func (t *textIn) walkDir(dir string, entries map[string]*lib.Entry) error { + err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + + if err := t.walkLocalFile(path, "", entries); err != nil { + return err + } + + return nil + }) + + return err +} + +func (t *textIn) walkLocalFile(path, name string, entries map[string]*lib.Entry) error { + name = strings.TrimSpace(name) + var filename string + if name != "" { + filename = name + } else { + filename = filepath.Base(path) + } + + // check filename + if !regexp.MustCompile(`^[a-zA-Z0-9_.\-]+$`).MatchString(filename) { + return fmt.Errorf("filename %s cannot be entry name, please remove special characters in it", filename) + } + dotIndex := strings.LastIndex(filename, ".") + if dotIndex > 0 { + filename = filename[:dotIndex] + } + + if _, found := entries[filename]; found { + return fmt.Errorf("found duplicated file %s", filename) + } + + entry := lib.NewEntry(filename) + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + if err := t.scanFile(file, entry); err != nil { + return err + } + + entries[filename] = entry + + return nil +} + +func (t *textIn) walkRemoteFile(url, name string, entries map[string]*lib.Entry) error { + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return fmt.Errorf("failed to get remote file %s, http status code %d", url, resp.StatusCode) + } + + entry := lib.NewEntry(name) + if err := t.scanFile(resp.Body, entry); err != nil { + return err + } + + entries[name] = entry + return nil +} + +func (t *textIn) scanFile(reader io.Reader, entry *lib.Entry) error { + scanner := bufio.NewScanner(reader) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" { + continue + } + if err := entry.AddPrefix(line); err != nil { + return err + } + } + + if err := scanner.Err(); err != nil { + return err + } + + return nil +} diff --git a/refilter-geoip/plugin/plaintext/text_out.go b/refilter-geoip/plugin/plaintext/text_out.go new file mode 100644 index 0000000..68f1d92 --- /dev/null +++ b/refilter-geoip/plugin/plaintext/text_out.go @@ -0,0 +1,166 @@ +package plaintext + +import ( + "bytes" + "encoding/json" + "log" + "os" + "path/filepath" + "strings" + + "github.com/v2fly/geoip/lib" +) + +const ( + typeTextOut = "text" + descTextOut = "Convert data to plaintext CIDR format" +) + +var ( + defaultOutputDir = filepath.Join("./", "output", "text") +) + +func init() { + lib.RegisterOutputConfigCreator(typeTextOut, func(action lib.Action, data json.RawMessage) (lib.OutputConverter, error) { + return newTextOut(action, data) + }) + lib.RegisterOutputConverter(typeTextOut, &textOut{ + Description: descTextOut, + }) +} + +func newTextOut(action lib.Action, data json.RawMessage) (lib.OutputConverter, error) { + var tmp struct { + OutputDir string `json:"outputDir"` + Want []string `json:"wantedList"` + OnlyIPType lib.IPType `json:"onlyIPType"` + } + + if len(data) > 0 { + if err := json.Unmarshal(data, &tmp); err != nil { + return nil, err + } + } + + if tmp.OutputDir == "" { + tmp.OutputDir = defaultOutputDir + } + + return &textOut{ + Type: typeTextOut, + Action: action, + Description: descTextOut, + OutputDir: tmp.OutputDir, + Want: tmp.Want, + OnlyIPType: tmp.OnlyIPType, + }, nil +} + +type textOut struct { + Type string + Action lib.Action + Description string + OutputDir string + Want []string + OnlyIPType lib.IPType +} + +func (t *textOut) GetType() string { + return t.Type +} + +func (t *textOut) GetAction() lib.Action { + return t.Action +} + +func (t *textOut) GetDescription() string { + return t.Description +} + +func (t *textOut) Output(container lib.Container) error { + // Filter want list + wantList := make(map[string]bool) + for _, want := range t.Want { + if want = strings.ToUpper(strings.TrimSpace(want)); want != "" { + wantList[want] = true + } + } + + switch len(wantList) { + case 0: + for entry := range container.Loop() { + cidrList, err := t.marshalText(entry) + if err != nil { + return err + } + filename := strings.ToLower(entry.GetName()) + ".txt" + if err := t.writeFile(filename, cidrList); err != nil { + return err + } + } + + default: + for name := range wantList { + entry, found := container.GetEntry(name) + if !found { + log.Printf("❌ entry %s not found", name) + continue + } + cidrList, err := t.marshalText(entry) + if err != nil { + return err + } + filename := strings.ToLower(entry.GetName()) + ".txt" + if err := t.writeFile(filename, cidrList); err != nil { + return err + } + } + } + + return nil +} + +func (t *textOut) marshalText(entry *lib.Entry) ([]string, error) { + var entryCidr []string + var err error + switch t.OnlyIPType { + case lib.IPv4: + entryCidr, err = entry.MarshalText(lib.IgnoreIPv6) + if err != nil { + return nil, err + } + case lib.IPv6: + entryCidr, err = entry.MarshalText(lib.IgnoreIPv4) + if err != nil { + return nil, err + } + default: + entryCidr, err = entry.MarshalText() + if err != nil { + return nil, err + } + } + + return entryCidr, nil +} + +func (t *textOut) writeFile(filename string, cidrList []string) error { + var buf bytes.Buffer + for _, cidr := range cidrList { + buf.WriteString(cidr) + buf.WriteString("\n") + } + cidrBytes := buf.Bytes() + + if err := os.MkdirAll(t.OutputDir, 0755); err != nil { + return err + } + + if err := os.WriteFile(filepath.Join(t.OutputDir, filename), cidrBytes, 0644); err != nil { + return err + } + + log.Printf("✅ [%s] %s --> %s", t.Type, filename, t.OutputDir) + + return nil +} diff --git a/refilter-geoip/plugin/special/cutter.go b/refilter-geoip/plugin/special/cutter.go new file mode 100644 index 0000000..8a8fdba --- /dev/null +++ b/refilter-geoip/plugin/special/cutter.go @@ -0,0 +1,96 @@ +package special + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/v2fly/geoip/lib" +) + +const ( + typeCutter = "cutter" + descCutter = "Remove data from previous steps" +) + +func init() { + lib.RegisterInputConfigCreator(typeCutter, func(action lib.Action, data json.RawMessage) (lib.InputConverter, error) { + return newCutter(action, data) + }) + lib.RegisterInputConverter(typeCutter, &cutter{ + Description: descCutter, + }) +} + +func newCutter(action lib.Action, data json.RawMessage) (lib.InputConverter, error) { + var tmp struct { + Want []string `json:"wantedList"` + OnlyIPType lib.IPType `json:"onlyIPType"` + } + + if len(data) > 0 { + if err := json.Unmarshal(data, &tmp); err != nil { + return nil, err + } + } + + if action != lib.ActionRemove { + return nil, fmt.Errorf("type %s only supports `remove` action", typeCutter) + } + + return &cutter{ + Type: typeCutter, + Action: action, + Description: descCutter, + Want: tmp.Want, + OnlyIPType: tmp.OnlyIPType, + }, nil +} + +type cutter struct { + Type string + Action lib.Action + Description string + Want []string + OnlyIPType lib.IPType +} + +func (c *cutter) GetType() string { + return c.Type +} + +func (c *cutter) GetAction() lib.Action { + return c.Action +} + +func (c *cutter) GetDescription() string { + return c.Description +} + +func (c *cutter) Input(container lib.Container) (lib.Container, error) { + var ignoreIPType lib.IgnoreIPOption + switch c.OnlyIPType { + case lib.IPv4: + ignoreIPType = lib.IgnoreIPv6 + case lib.IPv6: + ignoreIPType = lib.IgnoreIPv4 + } + + // Filter want list + wantList := make(map[string]bool) + for _, want := range c.Want { + if want = strings.ToUpper(strings.TrimSpace(want)); want != "" { + wantList[want] = true + } + } + + for entry := range container.Loop() { + name := entry.GetName() + if len(wantList) > 0 && !wantList[name] { + continue + } + container.Remove(name, ignoreIPType) + } + + return container, nil +} diff --git a/refilter-geoip/plugin/special/private.go b/refilter-geoip/plugin/special/private.go new file mode 100644 index 0000000..e9dceca --- /dev/null +++ b/refilter-geoip/plugin/special/private.go @@ -0,0 +1,94 @@ +package special + +import ( + "encoding/json" + + "github.com/v2fly/geoip/lib" +) + +const ( + entryNamePrivate = "private" + typePrivate = "private" + descPrivate = "Convert LAN and private network CIDR to other formats" +) + +var privateCIDRs = []string{ + "0.0.0.0/8", + "10.0.0.0/8", + "100.64.0.0/10", + "127.0.0.0/8", + "169.254.0.0/16", + "172.16.0.0/12", + "192.0.0.0/24", + "192.0.2.0/24", + "192.88.99.0/24", + "192.168.0.0/16", + "198.18.0.0/15", + "198.51.100.0/24", + "203.0.113.0/24", + "224.0.0.0/4", + "240.0.0.0/4", + "255.255.255.255/32", + "::/128", + "::1/128", + "fc00::/7", + "fe80::/10", + "ff00::/8", +} + +func init() { + lib.RegisterInputConfigCreator(typePrivate, func(action lib.Action, data json.RawMessage) (lib.InputConverter, error) { + return newPrivate(action, data) + }) + lib.RegisterInputConverter(typePrivate, &private{ + Description: descPrivate, + }) +} + +func newPrivate(action lib.Action, data json.RawMessage) (lib.InputConverter, error) { + return &private{ + Type: typePrivate, + Action: action, + Description: descPrivate, + }, nil +} + +type private struct { + Type string + Action lib.Action + Description string +} + +func (p *private) GetType() string { + return p.Type +} + +func (p *private) GetAction() lib.Action { + return p.Action +} + +func (p *private) GetDescription() string { + return p.Description +} + +func (p *private) Input(container lib.Container) (lib.Container, error) { + entry := lib.NewEntry(entryNamePrivate) + for _, cidr := range privateCIDRs { + if err := entry.AddPrefix(cidr); err != nil { + return nil, err + } + } + + switch p.Action { + case lib.ActionAdd: + if err := container.Add(entry); err != nil { + return nil, err + } + case lib.ActionRemove: + container.Remove(entryNamePrivate) + default: + return nil, lib.ErrUnknownAction + } + + return container, nil +} diff --git a/refilter-geoip/plugin/special/test.go b/refilter-geoip/plugin/special/test.go new file mode 100644 index 0000000..1a48893 --- /dev/null +++ b/refilter-geoip/plugin/special/test.go @@ -0,0 +1,74 @@ +package special + +import ( + "encoding/json" + + "github.com/v2fly/geoip/lib" +) + +const ( + entryNameTest = "test" + typeTest = "test" + descTest = "Convert specific CIDR to other formats (for test only)" +) + +var testCIDRs = []string{ + "127.0.0.0/8", +} + +func init() { + lib.RegisterInputConfigCreator(typeTest, func(action lib.Action, data json.RawMessage) (lib.InputConverter, error) { + return newTest(action, data) + }) + lib.RegisterInputConverter(typeTest, &test{ + Description: descTest, + }) +} + +func newTest(action lib.Action, data json.RawMessage) (lib.InputConverter, error) { + return &test{ + Type: typeTest, + Action: action, + Description: descTest, + }, nil +} + +type test struct { + Type string + Action lib.Action + Description string +} + +func (t *test) GetType() string { + return t.Type +} + +func (t *test) GetAction() lib.Action { + return t.Action +} + +func (t *test) GetDescription() string { + return t.Description +} + +func (t *test) Input(container lib.Container) (lib.Container, error) { + entry := lib.NewEntry(entryNameTest) + for _, cidr := range testCIDRs { + if err := entry.AddPrefix(cidr); err != nil { + return nil, err + } + } + + switch t.Action { + case lib.ActionAdd: + if err := container.Add(entry); err != nil { + return nil, err + } + case lib.ActionRemove: + container.Remove(entryNameTest) + default: + return nil, lib.ErrUnknownAction + } + + return container, nil +} diff --git a/refilter-geoip/plugin/v2ray/dat_in.go b/refilter-geoip/plugin/v2ray/dat_in.go new file mode 100644 index 0000000..03874a7 --- /dev/null +++ b/refilter-geoip/plugin/v2ray/dat_in.go @@ -0,0 +1,204 @@ +package v2ray + +import ( + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "os" + "strings" + + "github.com/v2fly/geoip/lib" + router "github.com/v2fly/v2ray-core/v5/app/router/routercommon" + "google.golang.org/protobuf/proto" +) + +const ( + typeGeoIPdatIn = "v2rayGeoIPDat" + descGeoIPdatIn = "Convert V2Ray GeoIP dat to other formats" +) + +func init() { + lib.RegisterInputConfigCreator(typeGeoIPdatIn, func(action lib.Action, data json.RawMessage) (lib.InputConverter, error) { + return newGeoIPDatIn(action, data) + }) + lib.RegisterInputConverter(typeGeoIPdatIn, &geoIPDatIn{ + Description: descGeoIPdatIn, + }) +} + +func newGeoIPDatIn(action lib.Action, data json.RawMessage) (lib.InputConverter, error) { + var tmp struct { + URI string `json:"uri"` + Want []string `json:"wantedList"` + OnlyIPType lib.IPType `json:"onlyIPType"` + } + + if len(data) > 0 { + if err := json.Unmarshal(data, &tmp); err != nil { + return nil, err + } + } + + if tmp.URI == "" { + return nil, fmt.Errorf("[type %s | action %s] uri must be specified in config", typeGeoIPdatIn, action) + } + + return &geoIPDatIn{ + Type: typeGeoIPdatIn, + Action: action, + Description: descGeoIPdatIn, + URI: tmp.URI, + Want: tmp.Want, + OnlyIPType: tmp.OnlyIPType, + }, nil +} + +type geoIPDatIn struct { + Type string + Action lib.Action + Description string + URI string + Want []string + OnlyIPType lib.IPType +} + +func (g *geoIPDatIn) GetType() string { + return g.Type +} + +func (g *geoIPDatIn) GetAction() lib.Action { + return g.Action +} + +func (g *geoIPDatIn) GetDescription() string { + return g.Description +} + +func (g *geoIPDatIn) Input(container lib.Container) (lib.Container, error) { + entries := make(map[string]*lib.Entry) + var err error + + switch { + case strings.HasPrefix(g.URI, "http://"), strings.HasPrefix(g.URI, "https://"): + err = g.walkRemoteFile(g.URI, entries) + default: + err = g.walkLocalFile(g.URI, entries) + } + + if err != nil { + return nil, err + } + + if len(entries) == 0 { + return nil, fmt.Errorf("❌ [type %s | action %s] no entry is newly generated", typeGeoIPdatIn, g.Action) + } + + var ignoreIPType lib.IgnoreIPOption + switch g.OnlyIPType { + case lib.IPv4: + ignoreIPType = lib.IgnoreIPv6 + case lib.IPv6: + ignoreIPType = lib.IgnoreIPv4 + } + + // Filter want list + wantList := make(map[string]bool) + for _, want := range g.Want { + if want = strings.ToUpper(strings.TrimSpace(want)); want != "" { + wantList[want] = true + } + } + + for _, entry := range entries { + name := entry.GetName() + if len(wantList) > 0 && !wantList[name] { + continue + } + + switch g.Action { + case lib.ActionAdd: + if err := container.Add(entry, ignoreIPType); err != nil { + return nil, err + } + case lib.ActionRemove: + container.Remove(name, ignoreIPType) + } + } + + return container, nil +} + +func (g *geoIPDatIn) walkLocalFile(path string, entries map[string]*lib.Entry) error { + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + + if err := g.generateEntries(file, entries); err != nil { + return err + } + + return nil +} + +func (g *geoIPDatIn) walkRemoteFile(url string, entries map[string]*lib.Entry) error { + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return fmt.Errorf("failed to get remote file %s, http status code %d", url, resp.StatusCode) + } + + if err := g.generateEntries(resp.Body, entries); err != nil { + return err + } + + return nil +} + +func (g *geoIPDatIn) generateEntries(reader io.Reader, entries map[string]*lib.Entry) error { + geoipBytes, err := io.ReadAll(reader) + if err != nil { + return err + } + + var geoipList router.GeoIPList + if err := proto.Unmarshal(geoipBytes, &geoipList); err != nil { + return err + } + + for _, geoip := range geoipList.Entry { + var entry *lib.Entry + name := geoip.CountryCode + if theEntry, found := entries[name]; found { + fmt.Printf("⚠️ [type %s | action %s] found duplicated entry: %s. Process anyway\n", typeGeoIPdatIn, g.Action, name) + entry = theEntry + } else { + entry = lib.NewEntry(name) + } + + for _, v2rayCIDR := range geoip.Cidr { + ipStr := net.IP(v2rayCIDR.GetIp()).String() + "/" + fmt.Sprint(v2rayCIDR.GetPrefix()) + switch g.Action { + case lib.ActionAdd: + if err := entry.AddPrefix(ipStr); err != nil { + return err + } + case lib.ActionRemove: + if err := entry.RemovePrefix(ipStr); err != nil { + return err + } + } + } + + entries[name] = entry + } + + return nil +} diff --git a/refilter-geoip/plugin/v2ray/dat_out.go b/refilter-geoip/plugin/v2ray/dat_out.go new file mode 100644 index 0000000..3c4ba23 --- /dev/null +++ b/refilter-geoip/plugin/v2ray/dat_out.go @@ -0,0 +1,227 @@ +package v2ray + +import ( + "encoding/json" + "fmt" + "log" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/v2fly/geoip/lib" + router "github.com/v2fly/v2ray-core/v5/app/router/routercommon" + "github.com/v2fly/v2ray-core/v5/infra/conf/rule" + "google.golang.org/protobuf/proto" +) + +const ( + typeGeoIPdatOut = "v2rayGeoIPDat" + descGeoIPdatOut = "Convert data to V2Ray GeoIP dat format" +) + +var ( + defaultOutputName = "geoip.dat" + defaultOutputDir = filepath.Join("./", "output", "dat") +) + +func init() { + lib.RegisterOutputConfigCreator(typeGeoIPdatOut, func(action lib.Action, data json.RawMessage) (lib.OutputConverter, error) { + return newGeoIPDat(action, data) + }) + lib.RegisterOutputConverter(typeGeoIPdatOut, &geoIPDatOut{ + Description: descGeoIPdatOut, + }) +} + +func newGeoIPDat(action lib.Action, data json.RawMessage) (lib.OutputConverter, error) { + var tmp struct { + OutputName string `json:"outputName"` + OutputDir string `json:"outputDir"` + Want []string `json:"wantedList"` + OneFilePerList bool `json:"oneFilePerList"` + OnlyIPType lib.IPType `json:"onlyIPType"` + } + + if len(data) > 0 { + if err := json.Unmarshal(data, &tmp); err != nil { + return nil, err + } + } + + if tmp.OutputName == "" { + tmp.OutputName = defaultOutputName + } + + if tmp.OutputDir == "" { + tmp.OutputDir = defaultOutputDir + } + + return &geoIPDatOut{ + Type: typeGeoIPdatOut, + Action: action, + Description: descGeoIPdatOut, + OutputName: tmp.OutputName, + OutputDir: tmp.OutputDir, + Want: tmp.Want, + OneFilePerList: tmp.OneFilePerList, + OnlyIPType: tmp.OnlyIPType, + }, nil +} + +type geoIPDatOut struct { + Type string + Action lib.Action + Description string + OutputName string + OutputDir string + Want []string + OneFilePerList bool + OnlyIPType lib.IPType +} + +func (g *geoIPDatOut) GetType() string { + return g.Type +} + +func (g *geoIPDatOut) GetAction() lib.Action { + return g.Action +} + +func (g *geoIPDatOut) GetDescription() string { + return g.Description +} + +func (g *geoIPDatOut) Output(container lib.Container) error { + // Filter want list + wantList := make(map[string]bool) + for _, want := range g.Want { + if want = strings.ToUpper(strings.TrimSpace(want)); want != "" { + wantList[want] = true + } + } + + geoIPList := new(router.GeoIPList) + geoIPList.Entry = make([]*router.GeoIP, 0, 300) + updated := false + switch len(wantList) { + case 0: + for entry := range container.Loop() { + geoIP, err := g.generateGeoIP(entry) + if err != nil { + return err + } + geoIPList.Entry = append(geoIPList.Entry, geoIP) + updated = true + + if g.OneFilePerList { + geoIPBytes, err := proto.Marshal(geoIPList) + if err != nil { + return err + } + filename := strings.ToLower(entry.GetName()) + ".dat" + if err := g.writeFile(filename, geoIPBytes); err != nil { + return err + } + geoIPList.Entry = nil + } + } + + default: + for name := range wantList { + entry, found := container.GetEntry(name) + if !found { + log.Printf("❌ entry %s not found", name) + continue + } + geoIP, err := g.generateGeoIP(entry) + if err != nil { + return err + } + geoIPList.Entry = append(geoIPList.Entry, geoIP) + updated = true + + if g.OneFilePerList { + geoIPBytes, err := proto.Marshal(geoIPList) + if err != nil { + return err + } + filename := strings.ToLower(entry.GetName()) + ".dat" + if err := g.writeFile(filename, geoIPBytes); err != nil { + return err + } + geoIPList.Entry = nil + } + } + } + + // Sort to make reproducible builds + g.sort(geoIPList) + + if !g.OneFilePerList && updated { + geoIPBytes, err := proto.Marshal(geoIPList) + if err != nil { + return err + } + if err := g.writeFile(g.OutputName, geoIPBytes); err != nil { + return err + } + } + + return nil +} + +func (g *geoIPDatOut) generateGeoIP(entry *lib.Entry) (*router.GeoIP, error) { + var entryCidr []string + var err error + switch g.OnlyIPType { + case lib.IPv4: + entryCidr, err = entry.MarshalText(lib.IgnoreIPv6) + case lib.IPv6: + entryCidr, err = entry.MarshalText(lib.IgnoreIPv4) + default: + entryCidr, err = entry.MarshalText() + } + if err != nil { + return nil, err + } + + v2rayCIDR := make([]*router.CIDR, 0, 1024) + for _, cidrStr := range entryCidr { + cidr, err := rule.ParseIP(cidrStr) + if err != nil { + return nil, err + } + v2rayCIDR = append(v2rayCIDR, cidr) + } + + if len(v2rayCIDR) > 0 { + return &router.GeoIP{ + CountryCode: entry.GetName(), + Cidr: v2rayCIDR, + }, nil + } + + return nil, fmt.Errorf("entry %s has no CIDR", entry.GetName()) +} + +// Sort by country code to make reproducible builds +func (g *geoIPDatOut) sort(list *router.GeoIPList) { + sort.SliceStable(list.Entry, func(i, j int) bool { + return list.Entry[i].CountryCode < list.Entry[j].CountryCode + }) +} + +func (g *geoIPDatOut) writeFile(filename string, geoIPBytes []byte) error { + if err := os.MkdirAll(g.OutputDir, 0755); err != nil { + return err + } + + if err := os.WriteFile(filepath.Join(g.OutputDir, filename), geoIPBytes, 0644); err != nil { + return err + } + + log.Printf("✅ [%s] %s --> %s", g.Type, filename, g.OutputDir) + + return nil +}