From e892733a55c9088b39531fbc4146c57f0312a0ac Mon Sep 17 00:00:00 2001 From: Christian Ditaputratama Date: Thu, 7 Nov 2024 20:26:49 +0700 Subject: [PATCH] feat: Added i2p support #148 For now, only p32 address is supported. ToDo: Accept i2p naming service from addressbook subscriptions ToDo: Imporve i2p UI display and add i2p filter --- .env.example | 2 ++ .github/workflows/test.yml | 1 + README.md | 1 + cmd/client/probe.go | 38 ++++++++++++++++++++---- cmd/cmd.go | 3 +- internal/config/app.go | 4 +++ internal/database/schema.go | 17 ++++++++++- internal/handler/response.go | 3 +- internal/handler/views/add_node.templ | 7 ++--- internal/handler/views/add_node_templ.go | 2 +- internal/monero/monero.go | 29 ++++++++++++++---- internal/monero/report.go | 10 +++++-- 12 files changed, 96 insertions(+), 21 deletions(-) diff --git a/.env.example b/.env.example index 8e9802a..86a0bcd 100644 --- a/.env.example +++ b/.env.example @@ -8,6 +8,8 @@ SERVER_ENDPOINT="http://127.0.0.1:18901" API_KEY= ACCEPT_TOR=false TOR_SOCKS="127.0.0.1:9050" +ACCEPT_I2P=false +I2P_SOCKS="127.0.0.1:4447" IPV6_CAPABLE=false # Server Config diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b762155..6a50940 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,6 +3,7 @@ on: branches: - main - htmx + - i2p-support pull_request: name: Test diff --git a/README.md b/README.md index ce14953..2d021c8 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,7 @@ See the [Makefile](./Makefile). - :white_check_mark: Accept IPv6 nodes. - :white_check_mark: Use `a-h/templ` and `HTMX` instead of `Svelte`. - Use Go standard `net/http` instead of `fiber`. +- Accept I2P nodes. ## Acknowledgement diff --git a/cmd/client/probe.go b/cmd/client/probe.go index 9fabb09..e5212db 100644 --- a/cmd/client/probe.go +++ b/cmd/client/probe.go @@ -21,11 +21,12 @@ import ( "golang.org/x/net/proxy" ) -const RPCUserAgent = "ditatombot/0.0.1 (Monero RPC Monitoring; https://github.com/ditatompel/xmr-remote-nodes)" +const RPCUserAgent = "ditatombot/0.0.2 (Monero RPC Monitoring; https://github.com/ditatompel/xmr-remote-nodes)" const ( errNoEndpoint = errProber("no SERVER_ENDPOINT was provided") errNoTorSocks = errProber("no TOR_SOCKS was provided") + errNoI2PSocks = errProber("no I2P_SOCKS was provided") errNoAPIKey = errProber("no API_KEY was provided") errInvalidCredentials = errProber("invalid API_KEY credentials") ) @@ -41,6 +42,8 @@ type proberClient struct { apiKey string // prober api key acceptTor bool // accept tor torSOCKS string // IP:Port of tor socks + acceptI2P bool // accept i2p + I2PSOCKS string // IP:Port of i2p socks acceptIPv6 bool // accept ipv6 message string // message to include when reporting back to server } @@ -52,6 +55,8 @@ func newProber() *proberClient { apiKey: cfg.APIKey, acceptTor: cfg.AcceptTor, torSOCKS: cfg.TorSOCKS, + acceptI2P: cfg.AcceptI2P, + I2PSOCKS: cfg.I2PSOCKS, acceptIPv6: cfg.IPv6Capable, } } @@ -67,6 +72,9 @@ var ProbeCmd = &cobra.Command{ if t, _ := cmd.Flags().GetBool("no-tor"); t { prober.SetAcceptTor(false) } + if t, _ := cmd.Flags().GetBool("no-i2p"); t { + prober.SetAcceptI2P(false) + } if err := prober.Run(); err != nil { switch err.(type) { @@ -88,6 +96,10 @@ func (p *proberClient) SetAcceptTor(acceptTor bool) { p.acceptTor = acceptTor } +func (p *proberClient) SetAcceptI2P(acceptI2P bool) { + p.acceptI2P = acceptI2P +} + func (p *proberClient) SetAcceptIPv6(acceptIPv6 bool) { p.acceptIPv6 = acceptIPv6 } @@ -122,6 +134,9 @@ func (p *proberClient) validateConfig() error { if p.acceptTor && p.torSOCKS == "" { return errNoTorSocks } + if p.acceptI2P && p.I2PSOCKS == "" { + return errNoI2PSocks + } return nil } @@ -133,6 +148,11 @@ func (p *proberClient) fetchJob() (monero.Node, error) { acceptTor = 1 } + acceptI2P := 0 + if p.acceptI2P { + acceptI2P = 1 + } + acceptIPv6 := 0 if p.acceptIPv6 { acceptIPv6 = 1 @@ -140,7 +160,7 @@ func (p *proberClient) fetchJob() (monero.Node, error) { var node monero.Node - uri := fmt.Sprintf("%s/api/v1/job?accept_tor=%d&accept_ipv6=%d", p.endpoint, acceptTor, acceptIPv6) + uri := fmt.Sprintf("%s/api/v1/job?accept_tor=%d&accept_i2p=%d&accept_ipv6=%d", p.endpoint, acceptTor, acceptI2P, acceptIPv6) slog.Info(fmt.Sprintf("[PROBE] Getting node from %s", uri)) req, err := http.NewRequest(http.MethodGet, uri, nil) @@ -198,8 +218,16 @@ func (p *proberClient) fetchNode(node monero.Node) (monero.Node, error) { req.Header.Set("Origin", "https://xmr.ditatompel.com") var client http.Client + var socks5 string + if p.acceptTor && node.IsTor { - dialer, err := proxy.SOCKS5("tcp", p.torSOCKS, nil, proxy.Direct) + socks5 = p.torSOCKS + } else if p.acceptI2P && node.IsI2P { + socks5 = p.I2PSOCKS + } + + if socks5 != "" { + dialer, err := proxy.SOCKS5("tcp", socks5, nil, proxy.Direct) if err != nil { return node, err } @@ -268,7 +296,7 @@ func (p *proberClient) fetchNode(node monero.Node) (monero.Node, error) { node.CORSCapable = true } - if !node.IsTor { + if !node.IsTor && !node.IsI2P { hostIp, err := net.LookupIP(node.Hostname) if err != nil { fmt.Println("Warning: Could not resolve hostname: " + node.Hostname) @@ -335,7 +363,7 @@ func (p *proberClient) fetchFee(client http.Client, endpoint string) (uint, erro } func (p *proberClient) reportResult(node monero.Node, tookTime float64) error { - if !node.IsTor { + if !node.IsTor && !node.IsI2P { if hostIps, err := net.LookupIP(node.Hostname); err == nil { node.IPv6Only = ip.IsIPv6Only(hostIps) node.IPAddresses = ip.SliceToString(hostIps) diff --git a/cmd/cmd.go b/cmd/cmd.go index 2759212..bcfd08c 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -29,7 +29,8 @@ func init() { Root.PersistentFlags().StringVarP(&configFile, "config-file", "c", "", "Default to .env") Root.AddCommand(client.ProbeCmd) client.ProbeCmd.Flags().StringP("endpoint", "e", "", "Server endpoint") - client.ProbeCmd.Flags().Bool("no-tor", false, "Only probe clearnet nodes") + client.ProbeCmd.Flags().Bool("no-tor", false, "Do not probe tor nodes") + client.ProbeCmd.Flags().Bool("no-i2p", false, "Do not probe i2p nodes") } func initConfig() { diff --git a/internal/config/app.go b/internal/config/app.go index af71eb7..4ac6ceb 100644 --- a/internal/config/app.go +++ b/internal/config/app.go @@ -27,6 +27,8 @@ type App struct { APIKey string AcceptTor bool TorSOCKS string + AcceptI2P bool + I2PSOCKS string IPv6Capable bool } @@ -72,5 +74,7 @@ func LoadApp() { app.APIKey = os.Getenv("API_KEY") app.AcceptTor, _ = strconv.ParseBool(os.Getenv("ACCEPT_TOR")) app.TorSOCKS = os.Getenv("TOR_SOCKS") + app.AcceptI2P, _ = strconv.ParseBool(os.Getenv("ACCEPT_I2P")) + app.I2PSOCKS = os.Getenv("I2P_SOCKS") app.IPv6Capable, _ = strconv.ParseBool(os.Getenv("IPV6_CAPABLE")) } diff --git a/internal/database/schema.go b/internal/database/schema.go index be1d206..8b05c38 100644 --- a/internal/database/schema.go +++ b/internal/database/schema.go @@ -7,7 +7,7 @@ import ( type migrateFn func(*DB) error -var dbMigrate = [...]migrateFn{v1, v2, v3} +var dbMigrate = [...]migrateFn{v1, v2, v3, v4} func MigrateDb(db *DB) error { version := getSchemaVersion(db) @@ -272,3 +272,18 @@ func v3(db *DB) error { return nil } + +func v4(db *DB) error { + slog.Debug("[DB] Migrating database schema version 4") + + // table: tbl_node + slog.Debug("[DB] Adding additional columns to tbl_node") + _, err := db.Exec(` + ALTER TABLE tbl_node + ADD COLUMN is_i2p TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' AFTER is_tor;`) + if err != nil { + return err + } + + return nil +} diff --git a/internal/handler/response.go b/internal/handler/response.go index 94ad689..2cbaeab 100644 --- a/internal/handler/response.go +++ b/internal/handler/response.go @@ -402,10 +402,11 @@ func (s *fiberServer) countriesAPI(c *fiber.Ctx) error { // This handler should protected by `s.checkProberMW` middleware. func (s *fiberServer) giveJobAPI(c *fiber.Ctx) error { acceptTor := c.QueryInt("accept_tor", 0) + acceptI2P := c.QueryInt("accept_i2p", 0) acceptIPv6 := c.QueryInt("accept_ipv6", 0) moneroRepo := monero.New() - node, err := moneroRepo.GiveJob(acceptTor, acceptIPv6) + node, err := moneroRepo.GiveJob(acceptTor, acceptI2P, acceptIPv6) if err != nil { return c.JSON(fiber.Map{ "status": "error", diff --git a/internal/handler/views/add_node.templ b/internal/handler/views/add_node.templ index 60064bd..65ec85f 100644 --- a/internal/handler/views/add_node.templ +++ b/internal/handler/views/add_node.templ @@ -7,11 +7,9 @@ templ AddNode() {
-

Add Monero Node

-

You can use this page to add known remote node to the system so my bots can monitor it.

@@ -25,6 +23,7 @@ templ AddNode() {
  • As an administrator of this instance, I have full rights to delete, and blacklist any submitted node with or without providing any reason.
  • +
  • I2P nodes monitoring is beta and currently only support b32 address.
@@ -32,9 +31,7 @@ templ AddNode() {
-

- Enter your Monero node information below (IPv6 host check is experimental): -

+

Enter your Monero node information below:

diff --git a/internal/handler/views/add_node_templ.go b/internal/handler/views/add_node_templ.go index 36ea5fc..9c8b2b0 100644 --- a/internal/handler/views/add_node_templ.go +++ b/internal/handler/views/add_node_templ.go @@ -37,7 +37,7 @@ func AddNode() templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

Add Monero Node

You can use this page to add known remote node to the system so my bots can monitor it.


Important Note

  • As an administrator of this instance, I have full rights to delete, and blacklist any submitted node with or without providing any reason.

Enter your Monero node information below (IPv6 host check is experimental):

Existing remote nodes can be found in /remote-nodes page.

") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

Add Monero Node

You can use this page to add known remote node to the system so my bots can monitor it.


Important Note

  • As an administrator of this instance, I have full rights to delete, and blacklist any submitted node with or without providing any reason.
  • I2P nodes monitoring is beta and currently only support b32 address.

Enter your Monero node information below:

Existing remote nodes can be found in /remote-nodes page.

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/internal/monero/monero.go b/internal/monero/monero.go index 7574763..200a2d8 100644 --- a/internal/monero/monero.go +++ b/internal/monero/monero.go @@ -35,6 +35,7 @@ type Node struct { Port uint `json:"port" db:"port"` Protocol string `json:"protocol" db:"protocol"` IsTor bool `json:"is_tor" db:"is_tor"` + IsI2P bool `json:"is_i2p" db:"is_i2p"` IsAvailable bool `json:"is_available" db:"is_available"` Nettype string `json:"nettype" db:"nettype"` Height uint `json:"height" db:"height"` @@ -196,14 +197,25 @@ func (r *moneroRepo) Add(protocol string, hostname string, port uint) error { is_tor := false if strings.HasSuffix(hostname, ".onion") { + if !validTorHostname(hostname) { + return errors.New("Invalid TOR v3 .onion hostname") + } is_tor = true } + is_i2p := false + if strings.HasSuffix(hostname, ".i2p") { + if !validI2PHostname(hostname) { + return errors.New("Invalid I2P hostname") + } + is_i2p = true + } + ipAddr := "" ips := "" ipv6_only := false - if !is_tor { + if !is_tor && !is_i2p { hostIps, err := net.LookupIP(hostname) if err != nil { return err @@ -221,10 +233,6 @@ func (r *moneroRepo) Add(protocol string, hostname string, port uint) error { ipAddr = hostIp.String() ips = ip.SliceToString(hostIps) - } else { - if !validTorHostname(hostname) { - return errors.New("Invalid TOR v3 .onion hostname") - } } row, err := r.db.Query(` @@ -252,6 +260,7 @@ func (r *moneroRepo) Add(protocol string, hostname string, port uint) error { hostname, port, is_tor, + is_i2p, nettype, ip_addr, lat, @@ -274,12 +283,14 @@ func (r *moneroRepo) Add(protocol string, hostname string, port uint) error { ?, ?, ?, + ?, ? )`, protocol, hostname, port, is_tor, + is_i2p, "", ipAddr, 0, @@ -304,6 +315,14 @@ func validTorHostname(hostname string) bool { return regexp.MustCompile(`^([a-z0-9-]+\.)*[a-z2-7]{56}\.onion$`).MatchString(hostname) } +// validI2PHostname checks if a given hostname is a valid p32 I2P address +// +// Old b32 addresses are always {52 chars}.b32.i2p and new ones are {56+ chars}.b32.i2p. +// See: https://geti2p.net/spec/b32encrypted +func validI2PHostname(hostname string) bool { + return regexp.MustCompile(`^[a-z2-7]{52,}\.b32\.i2p$`).MatchString(hostname) +} + func (r *moneroRepo) Delete(id uint) error { if _, err := r.db.Exec(`DELETE FROM tbl_node WHERE id = ?`, id); err != nil { return err diff --git a/internal/monero/report.go b/internal/monero/report.go index 674950c..0906069 100644 --- a/internal/monero/report.go +++ b/internal/monero/report.go @@ -109,7 +109,7 @@ func (r *moneroRepo) Logs(q QueryLogs) (FetchLogs, error) { } // GiveJob returns node that should be probed for the next time -func (r *moneroRepo) GiveJob(acceptTor, acceptIPv6 int) (Node, error) { +func (r *moneroRepo) GiveJob(acceptTor, acceptI2P, acceptIPv6 int) (Node, error) { args := []interface{}{} wq := []string{} where := "" @@ -118,6 +118,10 @@ func (r *moneroRepo) GiveJob(acceptTor, acceptIPv6 int) (Node, error) { wq = append(wq, "is_tor = ?") args = append(args, 0) } + if acceptI2P != 1 { + wq = append(wq, "is_i2p = ?") + args = append(args, 0) + } if acceptIPv6 != 1 { wq = append(wq, "ipv6_only = ?") args = append(args, 0) @@ -136,10 +140,11 @@ func (r *moneroRepo) GiveJob(acceptTor, acceptIPv6 int) (Node, error) { port, protocol, is_tor, + is_i2p, last_check_status FROM tbl_node - %s -- where query if any + %s ORDER BY last_checked ASC LIMIT 1`, where) @@ -149,6 +154,7 @@ func (r *moneroRepo) GiveJob(acceptTor, acceptIPv6 int) (Node, error) { &node.Port, &node.Protocol, &node.IsTor, + &node.IsI2P, &node.LastCheckStatus) if err != nil { return node, err