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() {
You can use this page to add known remote node to the system so my bots can monitor it.
- Enter your Monero node information below (IPv6 host check is experimental): -
+Enter your Monero node information below: