summaryrefslogtreecommitdiff
path: root/plugin
diff options
context:
space:
mode:
Diffstat (limited to 'plugin')
-rw-r--r--plugin/maxmind/country_csv.go216
-rw-r--r--plugin/maxmind/mmdb.go198
-rw-r--r--plugin/plaintext/clash_in.go36
-rw-r--r--plugin/plaintext/clash_out.go36
-rw-r--r--plugin/plaintext/common_in.go106
-rw-r--r--plugin/plaintext/common_out.go170
-rw-r--r--plugin/plaintext/surge_in.go26
-rw-r--r--plugin/plaintext/surge_out.go26
-rw-r--r--plugin/plaintext/text_in.go202
-rw-r--r--plugin/plaintext/text_out.go78
-rw-r--r--plugin/special/private.go94
-rw-r--r--plugin/special/test.go76
-rw-r--r--plugin/v2ray/dat.go227
13 files changed, 1491 insertions, 0 deletions
diff --git a/plugin/maxmind/country_csv.go b/plugin/maxmind/country_csv.go
new file mode 100644
index 00000000..6394e375
--- /dev/null
+++ b/plugin/maxmind/country_csv.go
@@ -0,0 +1,216 @@
+package maxmind
+
+import (
+ "encoding/csv"
+ "encoding/json"
+ "errors"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/Loyalsoldier/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)
+ case lib.ActionReplace:
+ container.Replace(entry, 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/plugin/maxmind/mmdb.go b/plugin/maxmind/mmdb.go
new file mode 100644
index 00000000..64f79178
--- /dev/null
+++ b/plugin/maxmind/mmdb.go
@@ -0,0 +1,198 @@
+package maxmind
+
+import (
+ "encoding/json"
+ "fmt"
+ "log"
+ "net"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/Loyalsoldier/geoip/lib"
+ "github.com/maxmind/mmdbwriter"
+ "github.com/maxmind/mmdbwriter/mmdbtype"
+)
+
+const (
+ typeMaxmindMMDB = "maxmindMMDB"
+ descMaxmindMMDB = "Convert data to MaxMind mmdb database format"
+)
+
+var (
+ defaultOutputName = "Country.mmdb"
+ defaultOutputDir = filepath.Join("./", "output", "maxmind")
+)
+
+func init() {
+ lib.RegisterOutputConfigCreator(typeMaxmindMMDB, func(action lib.Action, data json.RawMessage) (lib.OutputConverter, error) {
+ return newMMDB(action, data)
+ })
+ lib.RegisterOutputConverter(typeMaxmindMMDB, &mmdb{
+ Description: descMaxmindMMDB,
+ })
+}
+
+func newMMDB(action lib.Action, data json.RawMessage) (lib.OutputConverter, error) {
+ var tmp struct {
+ OutputName string `json:"outputName"`
+ 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.OutputName == "" {
+ tmp.OutputName = defaultOutputName
+ }
+
+ if tmp.OutputDir == "" {
+ tmp.OutputDir = defaultOutputDir
+ }
+
+ return &mmdb{
+ Type: typeMaxmindMMDB,
+ Action: action,
+ Description: descMaxmindMMDB,
+ OutputName: tmp.OutputName,
+ OutputDir: tmp.OutputDir,
+ Want: tmp.Want,
+ OnlyIPType: tmp.OnlyIPType,
+ }, nil
+}
+
+type mmdb struct {
+ Type string
+ Action lib.Action
+ Description string
+ OutputName string
+ OutputDir string
+ Want []string
+ OnlyIPType lib.IPType
+}
+
+func (m *mmdb) GetType() string {
+ return m.Type
+}
+
+func (m *mmdb) GetAction() lib.Action {
+ return m.Action
+}
+
+func (m *mmdb) GetDescription() string {
+ return m.Description
+}
+
+func (m *mmdb) Output(container lib.Container) error {
+ // Filter want list
+ wantList := make(map[string]bool)
+ for _, want := range m.Want {
+ if want = strings.ToUpper(strings.TrimSpace(want)); want != "" {
+ wantList[want] = true
+ }
+ }
+
+ writer, err := mmdbwriter.New(
+ mmdbwriter.Options{
+ DatabaseType: "GeoIP2-Country",
+ RecordSize: 24,
+ IncludeReservedNetworks: true,
+ },
+ )
+ if err != nil {
+ return err
+ }
+
+ updated := false
+ switch len(wantList) {
+ case 0:
+ for entry := range container.Loop() {
+ if err := m.marshalData(writer, entry); err != nil {
+ return err
+ }
+ updated = true
+ }
+
+ default:
+ for name := range wantList {
+ entry, found := container.GetEntry(name)
+ if !found {
+ log.Printf("entry %s not found", name)
+ continue
+ }
+ if err := m.marshalData(writer, entry); err != nil {
+ return err
+ }
+ updated = true
+ }
+ }
+
+ if updated {
+ if err := m.writeFile(m.OutputName, writer); err != nil {
+ return err
+ }
+ } else {
+ return fmt.Errorf("type %s | action %s failed to write file", m.Type, m.Action)
+ }
+
+ return nil
+}
+
+func (m *mmdb) marshalData(writer *mmdbwriter.Tree, entry *lib.Entry) error {
+ var entryCidr []string
+ var err error
+ switch m.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 err
+ }
+
+ record := mmdbtype.Map{
+ "country": mmdbtype.Map{
+ "iso_code": mmdbtype.String(entry.GetName()),
+ },
+ }
+
+ for _, cidr := range entryCidr {
+ _, network, err := net.ParseCIDR(cidr)
+ if err != nil {
+ return err
+ }
+ if err := writer.Insert(network, record); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (m *mmdb) writeFile(filename string, writer *mmdbwriter.Tree) error {
+ if err := os.MkdirAll(m.OutputDir, 0755); err != nil {
+ return err
+ }
+
+ f, err := os.OpenFile(filepath.Join(m.OutputDir, filename), os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644)
+ if err != nil {
+ return err
+ }
+
+ _, err = writer.WriteTo(f)
+ if err != nil {
+ return err
+ }
+
+ log.Printf("✅ [%s] %s --> %s", m.Type, filename, m.OutputDir)
+
+ return nil
+}
diff --git a/plugin/plaintext/clash_in.go b/plugin/plaintext/clash_in.go
new file mode 100644
index 00000000..7cf36338
--- /dev/null
+++ b/plugin/plaintext/clash_in.go
@@ -0,0 +1,36 @@
+package plaintext
+
+import (
+ "encoding/json"
+
+ "github.com/Loyalsoldier/geoip/lib"
+)
+
+/*
+The types in this file extend the type `typeTextIn`,
+which make it possible to support more formats for the project.
+*/
+
+const (
+ typeClashRuleSetClassicalIn = "clashRuleSetClassical"
+ descClashClassicalIn = "Convert classical type of Clash RuleSet to other formats (just processing IP & CIDR lines)"
+
+ typeClashRuleSetIPCIDRIn = "clashRuleSet"
+ descClashRuleSetIn = "Convert ipcidr type of Clash RuleSet to other formats"
+)
+
+func init() {
+ lib.RegisterInputConfigCreator(typeClashRuleSetClassicalIn, func(action lib.Action, data json.RawMessage) (lib.InputConverter, error) {
+ return newTextIn(typeClashRuleSetClassicalIn, action, data)
+ })
+ lib.RegisterInputConverter(typeClashRuleSetClassicalIn, &textIn{
+ Description: descClashClassicalIn,
+ })
+
+ lib.RegisterInputConfigCreator(typeClashRuleSetIPCIDRIn, func(action lib.Action, data json.RawMessage) (lib.InputConverter, error) {
+ return newTextIn(typeClashRuleSetIPCIDRIn, action, data)
+ })
+ lib.RegisterInputConverter(typeClashRuleSetIPCIDRIn, &textIn{
+ Description: descClashRuleSetIn,
+ })
+}
diff --git a/plugin/plaintext/clash_out.go b/plugin/plaintext/clash_out.go
new file mode 100644
index 00000000..a7feab75
--- /dev/null
+++ b/plugin/plaintext/clash_out.go
@@ -0,0 +1,36 @@
+package plaintext
+
+import (
+ "encoding/json"
+
+ "github.com/Loyalsoldier/geoip/lib"
+)
+
+/*
+The types in this file extend the type `typeTextOut`,
+which make it possible to support more formats for the project.
+*/
+
+const (
+ typeClashRuleSetClassicalOut = "clashRuleSetClassical"
+ descClashClassicalOut = "Convert data to classical type of Clash RuleSet"
+
+ typeClashRuleSetIPCIDROut = "clashRuleSet"
+ descClashRuleSetOut = "Convert data to ipcidr type of Clash RuleSet"
+)
+
+func init() {
+ lib.RegisterOutputConfigCreator(typeClashRuleSetClassicalOut, func(action lib.Action, data json.RawMessage) (lib.OutputConverter, error) {
+ return newTextOut(typeClashRuleSetClassicalOut, action, data)
+ })
+ lib.RegisterOutputConverter(typeClashRuleSetClassicalOut, &textOut{
+ Description: descClashClassicalOut,
+ })
+
+ lib.RegisterOutputConfigCreator(typeClashRuleSetIPCIDROut, func(action lib.Action, data json.RawMessage) (lib.OutputConverter, error) {
+ return newTextOut(typeClashRuleSetIPCIDROut, action, data)
+ })
+ lib.RegisterOutputConverter(typeClashRuleSetIPCIDROut, &textOut{
+ Description: descClashRuleSetOut,
+ })
+}
diff --git a/plugin/plaintext/common_in.go b/plugin/plaintext/common_in.go
new file mode 100644
index 00000000..c8bfe367
--- /dev/null
+++ b/plugin/plaintext/common_in.go
@@ -0,0 +1,106 @@
+package plaintext
+
+import (
+ "bufio"
+ "io"
+ "strings"
+
+ "github.com/Loyalsoldier/geoip/lib"
+ "gopkg.in/yaml.v2"
+)
+
+type textIn struct {
+ Type string
+ Action lib.Action
+ Description string
+ Name string
+ URI string
+ InputDir string
+ OnlyIPType lib.IPType
+}
+
+func (t *textIn) scanFile(reader io.Reader, entry *lib.Entry) error {
+ var err error
+ switch t.Type {
+ case typeTextIn:
+ err = t.scanFileForTextIn(reader, entry)
+ case typeClashRuleSetClassicalIn:
+ err = t.scanFileForClashClassicalRuleSetInAndSurgeIn(reader, entry)
+ case typeClashRuleSetIPCIDRIn:
+ err = t.scanFileForClashRuleSetIn(reader, entry)
+ case typeSurgeRuleSetIn:
+ err = t.scanFileForClashClassicalRuleSetInAndSurgeIn(reader, entry)
+ default:
+ return lib.ErrNotSupportedFormat
+ }
+
+ return err
+}
+
+func (t *textIn) scanFileForTextIn(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
+}
+
+func (t *textIn) scanFileForClashRuleSetIn(reader io.Reader, entry *lib.Entry) error {
+ var payload struct {
+ Payload []string `yaml:"payload"`
+ }
+
+ data, err := io.ReadAll(reader)
+ if err != nil {
+ return err
+ }
+
+ if err := yaml.Unmarshal(data, &payload); err != nil {
+ return err
+ }
+
+ for _, cidrStr := range payload.Payload {
+ if err := entry.AddPrefix(strings.TrimSpace(cidrStr)); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (t *textIn) scanFileForClashClassicalRuleSetInAndSurgeIn(reader io.Reader, entry *lib.Entry) error {
+ scanner := bufio.NewScanner(reader)
+ for scanner.Scan() {
+ line := strings.ToLower(strings.TrimSpace(scanner.Text()))
+ if line == "" {
+ continue
+ }
+
+ switch {
+ case strings.HasPrefix(line, "ip-cidr,"), strings.HasPrefix(line, "ip-cidr6,"):
+ parts := strings.Split(line, ",")
+ if len(parts) > 1 {
+ if err := entry.AddPrefix(strings.TrimSpace(parts[1])); err != nil {
+ return err
+ }
+ }
+ default:
+ continue
+ }
+ }
+ if err := scanner.Err(); err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/plugin/plaintext/common_out.go b/plugin/plaintext/common_out.go
new file mode 100644
index 00000000..ff4ab63b
--- /dev/null
+++ b/plugin/plaintext/common_out.go
@@ -0,0 +1,170 @@
+package plaintext
+
+import (
+ "bytes"
+ "encoding/json"
+ "log"
+ "net"
+ "os"
+ "path/filepath"
+
+ "github.com/Loyalsoldier/geoip/lib"
+)
+
+var (
+ defaultOutputDirForTextOut = filepath.Join("./", "output", "text")
+ defaultOutputDirForClashRuleSetClassicalOut = filepath.Join("./", "output", "clash", "classical")
+ defaultOutputDirForClashRuleSetIPCIDROut = filepath.Join("./", "output", "clash", "ipcidr")
+ defaultOutputDirForSurgeRuleSetOut = filepath.Join("./", "output", "surge")
+)
+
+type textOut struct {
+ Type string
+ Action lib.Action
+ Description string
+ OutputDir string
+ Want []string
+ OnlyIPType lib.IPType
+}
+
+func newTextOut(iType string, 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 == "" {
+ switch iType {
+ case typeTextOut:
+ tmp.OutputDir = defaultOutputDirForTextOut
+ case typeClashRuleSetClassicalOut:
+ tmp.OutputDir = defaultOutputDirForClashRuleSetClassicalOut
+ case typeClashRuleSetIPCIDROut:
+ tmp.OutputDir = defaultOutputDirForClashRuleSetIPCIDROut
+ case typeSurgeRuleSetOut:
+ tmp.OutputDir = defaultOutputDirForSurgeRuleSetOut
+ }
+ }
+
+ return &textOut{
+ Type: iType,
+ Action: action,
+ Description: descTextOut,
+ OutputDir: tmp.OutputDir,
+ Want: tmp.Want,
+ OnlyIPType: tmp.OnlyIPType,
+ }, nil
+}
+
+func (t *textOut) marshalBytes(entry *lib.Entry) ([]byte, error) {
+ var err error
+
+ var entryCidr []string
+ switch t.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
+ }
+
+ var buf bytes.Buffer
+ switch t.Type {
+ case typeTextOut:
+ err = t.marshalBytesForTextOut(&buf, entryCidr)
+ case typeClashRuleSetClassicalOut:
+ err = t.marshalBytesForClashRuleSetClassicalOut(&buf, entryCidr)
+ case typeClashRuleSetIPCIDROut:
+ err = t.marshalBytesForClashRuleSetIPCIDROut(&buf, entryCidr)
+ case typeSurgeRuleSetOut:
+ err = t.marshalBytesForSurgeRuleSetOut(&buf, entryCidr)
+ default:
+ return nil, lib.ErrNotSupportedFormat
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ return buf.Bytes(), nil
+}
+
+func (t *textOut) marshalBytesForTextOut(buf *bytes.Buffer, entryCidr []string) error {
+ for _, cidr := range entryCidr {
+ buf.WriteString(cidr)
+ buf.WriteString("\n")
+ }
+ return nil
+}
+
+func (t *textOut) marshalBytesForClashRuleSetClassicalOut(buf *bytes.Buffer, entryCidr []string) error {
+ buf.WriteString("payload:\n")
+ for _, cidr := range entryCidr {
+ ip, _, err := net.ParseCIDR(cidr)
+ if err != nil {
+ return err
+ }
+ if ip.To4() != nil {
+ buf.WriteString(" - IP-CIDR,")
+ } else {
+ buf.WriteString(" - IP-CIDR6,")
+ }
+ buf.WriteString(cidr)
+ buf.WriteString("\n")
+ }
+
+ return nil
+}
+
+func (t *textOut) marshalBytesForClashRuleSetIPCIDROut(buf *bytes.Buffer, entryCidr []string) error {
+ buf.WriteString("payload:\n")
+ for _, cidr := range entryCidr {
+ buf.WriteString(" - '")
+ buf.WriteString(cidr)
+ buf.WriteString("'\n")
+ }
+
+ return nil
+}
+
+func (t *textOut) marshalBytesForSurgeRuleSetOut(buf *bytes.Buffer, entryCidr []string) error {
+ for _, cidr := range entryCidr {
+ ip, _, err := net.ParseCIDR(cidr)
+ if err != nil {
+ return err
+ }
+ if ip.To4() != nil {
+ buf.WriteString("IP-CIDR,")
+ } else {
+ buf.WriteString("IP-CIDR6,")
+ }
+ buf.WriteString(cidr)
+ buf.WriteString("\n")
+ }
+
+ return nil
+}
+
+func (t *textOut) writeFile(filename string, data []byte) error {
+ if err := os.MkdirAll(t.OutputDir, 0755); err != nil {
+ return err
+ }
+
+ if err := os.WriteFile(filepath.Join(t.OutputDir, filename), data, 0644); err != nil {
+ return err
+ }
+
+ log.Printf("✅ [%s] %s --> %s", t.Type, filename, t.OutputDir)
+
+ return nil
+}
diff --git a/plugin/plaintext/surge_in.go b/plugin/plaintext/surge_in.go
new file mode 100644
index 00000000..d409a782
--- /dev/null
+++ b/plugin/plaintext/surge_in.go
@@ -0,0 +1,26 @@
+package plaintext
+
+import (
+ "encoding/json"
+
+ "github.com/Loyalsoldier/geoip/lib"
+)
+
+/*
+The types in this file extend the type `typeTextIn`,
+which make it possible to support more formats for the project.
+*/
+
+const (
+ typeSurgeRuleSetIn = "surgeRuleSet"
+ descSurgeRuleSetIn = "Convert Surge RuleSet to other formats (just processing IP & CIDR lines)"
+)
+
+func init() {
+ lib.RegisterInputConfigCreator(typeSurgeRuleSetIn, func(action lib.Action, data json.RawMessage) (lib.InputConverter, error) {
+ return newTextIn(typeSurgeRuleSetIn, action, data)
+ })
+ lib.RegisterInputConverter(typeSurgeRuleSetIn, &textIn{
+ Description: descSurgeRuleSetIn,
+ })
+}
diff --git a/plugin/plaintext/surge_out.go b/plugin/plaintext/surge_out.go
new file mode 100644
index 00000000..c3868423
--- /dev/null
+++ b/plugin/plaintext/surge_out.go
@@ -0,0 +1,26 @@
+package plaintext
+
+import (
+ "encoding/json"
+
+ "github.com/Loyalsoldier/geoip/lib"
+)
+
+/*
+The types in this file extend the type `typeTextOut`,
+which make it possible to support more formats for the project.
+*/
+
+const (
+ typeSurgeRuleSetOut = "surgeRuleSet"
+ descSurgeRuleSetOut = "Convert data to Surge RuleSet"
+)
+
+func init() {
+ lib.RegisterOutputConfigCreator(typeSurgeRuleSetOut, func(action lib.Action, data json.RawMessage) (lib.OutputConverter, error) {
+ return newTextOut(typeSurgeRuleSetOut, action, data)
+ })
+ lib.RegisterOutputConverter(typeSurgeRuleSetOut, &textOut{
+ Description: descSurgeRuleSetOut,
+ })
+}
diff --git a/plugin/plaintext/text_in.go b/plugin/plaintext/text_in.go
new file mode 100644
index 00000000..64ad3ad3
--- /dev/null
+++ b/plugin/plaintext/text_in.go
@@ -0,0 +1,202 @@
+package plaintext
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+
+ "github.com/Loyalsoldier/geoip/lib"
+)
+
+const (
+ typeTextIn = "text"
+ descTextIn = "Convert plaintext IP & CIDR to other formats"
+)
+
+func init() {
+ lib.RegisterInputConfigCreator(typeTextIn, func(action lib.Action, data json.RawMessage) (lib.InputConverter, error) {
+ return newTextIn(typeTextIn, action, data)
+ })
+ lib.RegisterInputConverter(typeTextIn, &textIn{
+ Description: descTextIn,
+ })
+}
+
+func newTextIn(iType string, 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 strings.TrimSpace(iType) == "" {
+ return nil, fmt.Errorf("type is required")
+ }
+
+ 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: iType,
+ Action: action,
+ Description: descTextIn,
+ Name: tmp.Name,
+ URI: tmp.URI,
+ InputDir: tmp.InputDir,
+ OnlyIPType: tmp.OnlyIPType,
+ }, nil
+}
+
+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)
+ case lib.ActionReplace:
+ container.Replace(entry, 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
+}
diff --git a/plugin/plaintext/text_out.go b/plugin/plaintext/text_out.go
new file mode 100644
index 00000000..7bdfaf42
--- /dev/null
+++ b/plugin/plaintext/text_out.go
@@ -0,0 +1,78 @@
+package plaintext
+
+import (
+ "encoding/json"
+ "log"
+ "strings"
+
+ "github.com/Loyalsoldier/geoip/lib"
+)
+
+const (
+ typeTextOut = "text"
+ descTextOut = "Convert data to plaintext CIDR format"
+)
+
+func init() {
+ lib.RegisterOutputConfigCreator(typeTextOut, func(action lib.Action, data json.RawMessage) (lib.OutputConverter, error) {
+ return newTextOut(typeTextOut, action, data)
+ })
+ lib.RegisterOutputConverter(typeTextOut, &textOut{
+ Description: descTextOut,
+ })
+}
+
+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() {
+ data, err := t.marshalBytes(entry)
+ if err != nil {
+ return err
+ }
+ filename := strings.ToLower(entry.GetName()) + ".txt"
+ if err := t.writeFile(filename, data); err != nil {
+ return err
+ }
+ }
+
+ default:
+ for name := range wantList {
+ entry, found := container.GetEntry(name)
+ if !found {
+ log.Printf("entry %s not found", name)
+ continue
+ }
+ data, err := t.marshalBytes(entry)
+ if err != nil {
+ return err
+ }
+ filename := strings.ToLower(entry.GetName()) + ".txt"
+ if err := t.writeFile(filename, data); err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+}
diff --git a/plugin/special/private.go b/plugin/special/private.go
new file mode 100644
index 00000000..e8f231ef
--- /dev/null
+++ b/plugin/special/private.go
@@ -0,0 +1,94 @@
+package special
+
+import (
+ "encoding/json"
+
+ "github.com/Loyalsoldier/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",
+ "::1/128",
+ "fc00::/7",
+ "fe80::/10",
+}
+
+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)
+ case lib.ActionReplace:
+ container.Replace(entry)
+ default:
+ return nil, lib.ErrUnknownAction
+ }
+
+ return container, nil
+}
diff --git a/plugin/special/test.go b/plugin/special/test.go
new file mode 100644
index 00000000..2700b61e
--- /dev/null
+++ b/plugin/special/test.go
@@ -0,0 +1,76 @@
+package special
+
+import (
+ "encoding/json"
+
+ "github.com/Loyalsoldier/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)
+ case lib.ActionReplace:
+ container.Replace(entry)
+ default:
+ return nil, lib.ErrUnknownAction
+ }
+
+ return container, nil
+}
diff --git a/plugin/v2ray/dat.go b/plugin/v2ray/dat.go
new file mode 100644
index 00000000..b33d6090
--- /dev/null
+++ b/plugin/v2ray/dat.go
@@ -0,0 +1,227 @@
+package v2ray
+
+import (
+ "encoding/json"
+ "fmt"
+ "log"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+
+ "github.com/Loyalsoldier/geoip/lib"
+ "github.com/v2fly/v2ray-core/v4/app/router"
+ "github.com/v2fly/v2ray-core/v4/infra/conf/rule"
+ "google.golang.org/protobuf/proto"
+)
+
+const (
+ typeGeoIPdat = "v2rayGeoIPDat"
+ descGeoIPdat = "Convert data to V2Ray GeoIP dat format"
+)
+
+var (
+ defaultOutputName = "geoip.dat"
+ defaultOutputDir = filepath.Join("./", "output", "dat")
+)
+
+func init() {
+ lib.RegisterOutputConfigCreator(typeGeoIPdat, func(action lib.Action, data json.RawMessage) (lib.OutputConverter, error) {
+ return newGeoIPDat(action, data)
+ })
+ lib.RegisterOutputConverter(typeGeoIPdat, &geoIPDat{
+ Description: descGeoIPdat,
+ })
+}
+
+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 &geoIPDat{
+ Type: typeGeoIPdat,
+ Action: action,
+ Description: descGeoIPdat,
+ OutputName: tmp.OutputName,
+ OutputDir: tmp.OutputDir,
+ Want: tmp.Want,
+ OneFilePerList: tmp.OneFilePerList,
+ OnlyIPType: tmp.OnlyIPType,
+ }, nil
+}
+
+type geoIPDat struct {
+ Type string
+ Action lib.Action
+ Description string
+ OutputName string
+ OutputDir string
+ Want []string
+ OneFilePerList bool
+ OnlyIPType lib.IPType
+}
+
+func (g *geoIPDat) GetType() string {
+ return g.Type
+}
+
+func (g *geoIPDat) GetAction() lib.Action {
+ return g.Action
+}
+
+func (g *geoIPDat) GetDescription() string {
+ return g.Description
+}
+
+func (g *geoIPDat) 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 *geoIPDat) 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 *geoIPDat) sort(list *router.GeoIPList) {
+ sort.SliceStable(list.Entry, func(i, j int) bool {
+ return list.Entry[i].CountryCode < list.Entry[j].CountryCode
+ })
+}
+
+func (g *geoIPDat) 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
+}