From 46f5bf9660d333d9342dbb9fdac968a97739c0a3 Mon Sep 17 00:00:00 2001 From: Maxim Merzhanov Date: Sat, 11 Jan 2025 19:12:33 +0300 Subject: [PATCH 1/3] api: add tests for socks5 related api, refactor code to service, don't display offline peers available for socks5 proxy --- api/api.go | 6 ++++-- api/apiclient/client.go | 19 ++++++++++++++++++- api/settings.go | 15 +-------------- application.go | 2 +- application_test.go | 39 ++++++++++++++++++++++++++++++++++++++- service/auth_status.go | 1 + service/socks5.go | 24 ++++++++++++++++++++++++ 7 files changed, 87 insertions(+), 19 deletions(-) diff --git a/api/api.go b/api/api.go index f69ae3f..88328e0 100644 --- a/api/api.go +++ b/api/api.go @@ -32,6 +32,7 @@ type Handler struct { p2p *p2p.P2p authStatus *service.AuthStatus tunnel *service.Tunnel + socks5 *service.SOCKS5 dns DNSService logBuffer *ringbuffer.RingBuffer @@ -42,14 +43,15 @@ type Handler struct { ctxCancel context.CancelFunc } -func NewHandler(conf *config.Config, p2p *p2p.P2p, authStatus *service.AuthStatus, - tunnel *service.Tunnel, logBuffer *ringbuffer.RingBuffer, dns DNSService) *Handler { +func NewHandler(conf *config.Config, p2p *p2p.P2p, authStatus *service.AuthStatus, tunnel *service.Tunnel, socks5 *service.SOCKS5, + logBuffer *ringbuffer.RingBuffer, dns DNSService) *Handler { ctx, ctxCancel := context.WithCancel(context.Background()) return &Handler{ conf: conf, p2p: p2p, authStatus: authStatus, tunnel: tunnel, + socks5: socks5, dns: dns, logBuffer: logBuffer, logger: log.Logger("awl/api"), diff --git a/api/apiclient/client.go b/api/apiclient/client.go index e6dafb5..c981387 100644 --- a/api/apiclient/client.go +++ b/api/apiclient/client.go @@ -8,10 +8,11 @@ import ( "net/url" "time" + "github.com/google/go-querystring/query" + "github.com/anywherelan/awl/api" "github.com/anywherelan/awl/config" "github.com/anywherelan/awl/entity" - "github.com/google/go-querystring/query" ) type Client struct { @@ -57,6 +58,22 @@ func (c *Client) PeerInfo() (*entity.PeerInfo, error) { return peerInfo, nil } +func (c *Client) ListAvailableProxies() ([]entity.AvailableProxy, error) { + resp := entity.ListAvailableProxiesResponse{} + err := c.sendGetRequest(api.ListAvailableProxiesPath, &resp) + if err != nil { + return nil, err + } + return resp.Proxies, nil +} + +func (c *Client) UpdateProxySettings(usingPeerID string) error { + request := entity.UpdateProxySettingsRequest{ + UsingPeerID: usingPeerID, + } + return c.sendPostRequest(api.UpdateProxySettingsPath, request, nil) +} + func (c *Client) SendFriendRequest(peerID, alias string) error { request := entity.FriendRequest{ PeerID: peerID, diff --git a/api/settings.go b/api/settings.go index 3c581bb..7b86469 100644 --- a/api/settings.go +++ b/api/settings.go @@ -94,20 +94,7 @@ func (h *Handler) ExportServerConfiguration(c echo.Context) (err error) { // @Success 200 {object} entity.ListAvailableProxiesResponse // @Router /settings/list_proxies [GET] func (h *Handler) ListAvailableProxies(c echo.Context) (err error) { - h.conf.RLock() - proxies := []entity.AvailableProxy{} - for _, peer := range h.conf.KnownPeers { - if !peer.AllowedUsingAsExitNode { - continue - } - - proxy := entity.AvailableProxy{ - PeerID: peer.PeerID, - PeerName: peer.DisplayName(), - } - proxies = append(proxies, proxy) - } - h.conf.RUnlock() + proxies := h.socks5.ListAvailableProxies() response := entity.ListAvailableProxiesResponse{ Proxies: proxies, diff --git a/application.go b/application.go index 2784df5..1bb39ef 100644 --- a/application.go +++ b/application.go @@ -131,7 +131,7 @@ func (a *Application) Init(ctx context.Context, tunDevice tun.Device) error { a.Tunnel.RefreshPeersList() }, a.Eventbus, new(awlevent.KnownPeerChanged)) - handler := api.NewHandler(a.Conf, a.P2p, a.AuthStatus, a.Tunnel, a.LogBuffer, a.Dns) + handler := api.NewHandler(a.Conf, a.P2p, a.AuthStatus, a.Tunnel, a.SOCKS5, a.LogBuffer, a.Dns) a.Api = handler err = handler.SetupAPI() if err != nil { diff --git a/application_test.go b/application_test.go index 33b439a..370f62c 100644 --- a/application_test.go +++ b/application_test.go @@ -200,6 +200,14 @@ func TestUpdateUseAsExitNodeConfig(t *testing.T) { current := goleak.IgnoreCurrent() goleak.VerifyNone(t, current) + info, err := peer1.api.PeerInfo() + ts.NoError(err) + ts.Equal("", info.SOCKS5.UsingPeerID) + + availableProxies, err := peer1.api.ListAvailableProxies() + ts.NoError(err) + ts.Len(availableProxies, 0) + peer1Config, err := peer2.api.KnownPeerConfig(peer1.PeerID()) ts.NoError(err) ts.Equal(false, peer1Config.AllowedUsingAsExitNode) @@ -221,7 +229,13 @@ func TestUpdateUseAsExitNodeConfig(t *testing.T) { return peer2Config.AllowedUsingAsExitNode }, 15*time.Second, 100*time.Millisecond) - ts.Equal(peer2.PeerID(), peer1.app.Conf.SOCKS5.UsingPeerID) + info, err = peer1.api.PeerInfo() + ts.NoError(err) + ts.Equal(peer2.PeerID(), info.SOCKS5.UsingPeerID) + + availableProxies, err = peer1.api.ListAvailableProxies() + ts.NoError(err) + ts.Len(availableProxies, 1) // allow from peer1, check that peer2 got our config err = peer1.api.UpdatePeerSettings(entity.UpdatePeerSettingsRequest{ @@ -258,6 +272,11 @@ func TestUpdateUseAsExitNodeConfig(t *testing.T) { }, 15*time.Second, 100*time.Millisecond) ts.Equal("", peer1.app.Conf.SOCKS5.UsingPeerID) + + availableProxies, err = peer1.api.ListAvailableProxies() + ts.NoError(err) + ts.Len(availableProxies, 0) + testSOCKS5Proxy(ts, peer1.app.Conf.SOCKS5.ListenAddress, fmt.Sprintf("%s %s", "unknown error", "general SOCKS server failure")) testSOCKS5Proxy(ts, peer2.app.Conf.SOCKS5.ListenAddress, fmt.Sprintf("%s %s", "unknown error", "connection not allowed by ruleset")) @@ -265,6 +284,24 @@ func TestUpdateUseAsExitNodeConfig(t *testing.T) { peer1.app.SOCKS5.SetProxyingLocalhostEnabled(true) testSOCKS5Proxy(ts, peer2.app.Conf.SOCKS5.ListenAddress, "") peer1.app.SOCKS5.SetProxyingLocalhostEnabled(false) + + // Testing API + err = peer1.api.UpdateProxySettings(peer2.PeerID()) + ts.ErrorContains(err, "peer doesn't allow using as exit node") + + err = peer2.api.UpdateProxySettings("asd") + ts.ErrorContains(err, "peer not found") + + info, err = peer2.api.PeerInfo() + ts.NoError(err) + ts.Equal(peer1.PeerID(), info.SOCKS5.UsingPeerID) + + err = peer2.api.UpdateProxySettings("") + ts.NoError(err) + + info, err = peer2.api.PeerInfo() + ts.NoError(err) + ts.Equal("", info.SOCKS5.UsingPeerID) } func testSOCKS5Proxy(ts *TestSuite, proxyAddr string, expectSocksErr string) { diff --git a/service/auth_status.go b/service/auth_status.go index cd89ffa..abc12f1 100644 --- a/service/auth_status.go +++ b/service/auth_status.go @@ -26,6 +26,7 @@ const ( type P2p interface { ConnectPeer(ctx context.Context, peerID peer.ID) error + IsConnected(peerID peer.ID) bool NewStream(ctx context.Context, id peer.ID, proto libp2pProtocol.ID) (network.Stream, error) NewStreamWithDedicatedConn(ctx context.Context, id peer.ID, proto libp2pProtocol.ID) (network.Stream, error) SubscribeConnectionEvents(onConnected, onDisconnected func(network.Network, network.Conn)) diff --git a/service/socks5.go b/service/socks5.go index 9cfef15..bcd9909 100644 --- a/service/socks5.go +++ b/service/socks5.go @@ -14,6 +14,7 @@ import ( "github.com/libp2p/go-libp2p/core/network" "github.com/anywherelan/awl/config" + "github.com/anywherelan/awl/entity" "github.com/anywherelan/awl/protocol" "github.com/anywherelan/awl/socks5" ) @@ -59,6 +60,29 @@ func (s *SOCKS5) Close() { } } +func (s *SOCKS5) ListAvailableProxies() []entity.AvailableProxy { + s.conf.RLock() + proxies := []entity.AvailableProxy{} + for _, peer := range s.conf.KnownPeers { + if !peer.AllowedUsingAsExitNode { + continue + } + + if !s.p2p.IsConnected(peer.PeerId()) { + continue + } + + proxy := entity.AvailableProxy{ + PeerID: peer.PeerID, + PeerName: peer.DisplayName(), + } + proxies = append(proxies, proxy) + } + s.conf.RUnlock() + + return proxies +} + func (s *SOCKS5) ProxyStreamHandler(stream network.Stream) { defer func() { _ = stream.Reset() From 909c91f67d8d1dc9283b5aec2b692e07ba1efd36 Mon Sep 17 00:00:00 2001 From: Maxim Merzhanov Date: Sat, 11 Jan 2025 19:13:41 +0300 Subject: [PATCH 2/3] socks5: improve test, remove hardcoded ports --- socks5/server_test.go | 54 +++++++++++++++++++++++++++++++++---------- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/socks5/server_test.go b/socks5/server_test.go index 843568e..37d0882 100644 --- a/socks5/server_test.go +++ b/socks5/server_test.go @@ -1,50 +1,80 @@ package socks5 import ( + "context" "fmt" "io" + "net" "net/http" + "net/url" + "sync" "testing" "github.com/stretchr/testify/require" - "golang.org/x/net/proxy" ) // How to test // curl --socks5 localhost:3030 https://api.ipify.org -// TODO: prettify test, reuse code from application_test.go func TestProxy(t *testing.T) { - const listenAddr = "localhost:8673" + listenAddr := pickFreeAddr(t) socksServer := NewServer() socksServer.SetRules(NewRulePermitAll()) socksClient, err := NewClient(listenAddr) require.NoError(t, err) + + wg := &sync.WaitGroup{} + wg.Add(1) go func() { + defer wg.Done() + conn := <-socksClient.ConnsChan() socksServer.ServeConn(conn) }() - dialer, err := proxy.SOCKS5("tcp", listenAddr, nil, nil) - require.NoError(t, err) - + upstreamAddr := pickFreeAddr(t) mux := http.NewServeMux() mux.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) { _, _ = fmt.Fprintf(w, "test text") }) + //nolint + httpServer := &http.Server{Addr: upstreamAddr, Handler: mux} go func() { - // TODO: handle properly - //nolint - _ = http.ListenAndServe(":3030", mux) + _ = httpServer.ListenAndServe() + }() + defer func() { + httpServer.Shutdown(context.Background()) }() - httpTransport := &http.Transport{DialContext: dialer.(proxy.ContextDialer).DialContext} + + httpTransport := &http.Transport{ + Proxy: func(*http.Request) (*url.URL, error) { + return &url.URL{ + Scheme: "socks5", + Host: listenAddr, + }, nil + }, + } httpClient := http.Client{Transport: httpTransport} - response, err := httpClient.Get("http://localhost:3030/test") + response, err := httpClient.Get(fmt.Sprintf("http://%s/test", upstreamAddr)) require.NoError(t, err) - defer response.Body.Close() body, err := io.ReadAll(response.Body) require.NoError(t, err) + err = response.Body.Close() + require.NoError(t, err) require.Equal(t, "test text", string(body)) + + httpTransport.CloseIdleConnections() + wg.Wait() +} + +func pickFreeAddr(t testing.TB) string { + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer l.Close() + + return l.Addr().String() } From 91bfd0ecedef1481e286e772c4e192b65aed7778 Mon Sep 17 00:00:00 2001 From: Maxim Merzhanov Date: Sun, 12 Jan 2025 10:39:31 +0300 Subject: [PATCH 3/3] cli: display info for socks5 proxy, add commands `me list_proxies` and `me set_proxy` --- cli/cli.go | 66 ++++++++++++++++++++++++++++++++++++++++------------ cli/me.go | 50 +++++++++++++++++++++++++++++++++------ cli/peers.go | 4 ++++ 3 files changed, 98 insertions(+), 22 deletions(-) diff --git a/cli/cli.go b/cli/cli.go index 88282df..c819f27 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -11,12 +11,13 @@ import ( "strings" "github.com/GrigoryKrasnochub/updaterini" - "github.com/anywherelan/awl/api/apiclient" - "github.com/anywherelan/awl/config" - "github.com/anywherelan/awl/update" "github.com/ipfs/go-log/v2" "github.com/libp2p/go-libp2p/p2p/host/eventbus" "github.com/urfave/cli/v2" + + "github.com/anywherelan/awl/api/apiclient" + "github.com/anywherelan/awl/config" + "github.com/anywherelan/awl/update" ) const ( @@ -86,10 +87,9 @@ func (a *Application) init() { Usage: "Group of commands to work with your status and settings", Subcommands: []*cli.Command{ { - Name: "status", - Aliases: []string{"stats"}, - Usage: "Print your server status, network stats", - Before: a.initApiConnection, + Name: "status", + Usage: "Print your server status, network stats", + Before: a.initApiConnection, Action: func(c *cli.Context) error { return printStatus(a.api) }, @@ -117,6 +117,36 @@ func (a *Application) init() { return renameMe(a.api, c.String("name")) }, }, + { + Name: "list_proxies", + Usage: "Prints list of available SOCKS5 proxies", + Before: a.initApiConnection, + Action: func(c *cli.Context) error { + return listProxies(a.api) + }, + }, + { + Name: "set_proxy", + Usage: "Sets SOCKS5 proxy for your peer, empty pid/name means disable proxy", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "pid", + Usage: "peer id", + Required: false, + }, + &cli.StringFlag{ + Name: "name", + Usage: "peer name", + Required: false, + }, + }, + Before: func(c *cli.Context) error { + return a.initApiAndPeerId(c, false) + }, + Action: func(c *cli.Context) error { + return setProxy(a.api, c.String("pid")) + }, + }, }, }, { @@ -131,10 +161,10 @@ func (a *Application) init() { Name: "format", Aliases: []string{"f"}, Required: false, - Value: "npslucv", + Value: "npslucev", Usage: "control table columns list and order.Each char add column, write column chars together without gap. Use these chars to add specific columns:\n " + "n - peers number\n p - peers name, domain and ip address\n i - peers id\n s - peers status\n l - peers last seen datetime\n v - peers awl version" + - "\n u - network usage by peer (in/out)\n c - list of peers connections (IP address + protocol)\n ", + "\n u - network usage by peer (in/out)\n c - list of peers connections (IP address + protocol)\n e - exit node status\n ", }, }, Before: a.initApiConnection, @@ -185,7 +215,7 @@ func (a *Application) init() { Required: false, }, }, - Before: a.initApiAndPeerId, + Before: a.initApiAndPeerIdRequired, Action: func(c *cli.Context) error { return removePeer(a.api, c.String("pid")) }, @@ -210,7 +240,7 @@ func (a *Application) init() { Required: true, }, }, - Before: a.initApiAndPeerId, + Before: a.initApiAndPeerIdRequired, Action: func(c *cli.Context) error { return changePeerAlias(a.api, c.String("pid"), c.String("new_name")) }, @@ -235,7 +265,7 @@ func (a *Application) init() { Required: true, }, }, - Before: a.initApiAndPeerId, + Before: a.initApiAndPeerIdRequired, Action: func(c *cli.Context) error { return changePeerDomain(a.api, c.String("pid"), c.String("domain")) }, @@ -260,7 +290,7 @@ func (a *Application) init() { Required: false, }, }, - Before: a.initApiAndPeerId, + Before: a.initApiAndPeerIdRequired, Action: func(c *cli.Context) error { return setAllowUsingAsExitNode(a.api, c.String("pid"), c.Bool("allow")) }, @@ -408,7 +438,11 @@ func (a *Application) initApiConnection(c *cli.Context) (err error) { return nil } -func (a *Application) initApiAndPeerId(c *cli.Context) error { +func (a *Application) initApiAndPeerIdRequired(c *cli.Context) error { + return a.initApiAndPeerId(c, true) +} + +func (a *Application) initApiAndPeerId(c *cli.Context, isRequired bool) error { err := a.initApiConnection(c) if err != nil { return err @@ -419,8 +453,10 @@ func (a *Application) initApiAndPeerId(c *cli.Context) error { return nil } alias := c.String("name") - if alias == "" { + if alias == "" && isRequired { return fmt.Errorf("peerID or name should be defined") + } else if alias == "" && !isRequired { + return nil } pid, err = getPeerIdByAlias(a.api, alias) diff --git a/cli/me.go b/cli/me.go index 094109b..4f20908 100644 --- a/cli/me.go +++ b/cli/me.go @@ -6,9 +6,10 @@ import ( "strings" "time" - "github.com/anywherelan/awl/api/apiclient" "github.com/mdp/qrterminal/v3" "github.com/olekukonko/tablewriter" + + "github.com/anywherelan/awl/api/apiclient" ) func printStatus(api *apiclient.Client) error { @@ -17,17 +18,15 @@ func printStatus(api *apiclient.Client) error { return err } - dnsStatus := "working" - if !stats.IsAwlDNSSetAsSystem { - dnsStatus = "not working" - } - table := tablewriter.NewWriter(os.Stdout) table.AppendBulk([][]string{ {"Download rate", fmt.Sprintf("%s (%s)", stats.NetworkStatsInIECUnits.RateIn, stats.NetworkStatsInIECUnits.TotalIn)}, {"Upload rate", fmt.Sprintf("%s (%s)", stats.NetworkStatsInIECUnits.RateOut, stats.NetworkStatsInIECUnits.TotalOut)}, {"Bootstrap peers", fmt.Sprintf("%d/%d", stats.TotalBootstrapPeers, stats.ConnectedBootstrapPeers)}, - {"DNS", dnsStatus}, + {"DNS", formatWorkingStatus(stats.IsAwlDNSSetAsSystem)}, + {"SOCKS5 Proxy", formatWorkingStatus(stats.SOCKS5.ListenerEnabled)}, + {"SOCKS5 Proxy address", stats.SOCKS5.ListenAddress}, + {"SOCKS5 Proxy exit node", stats.SOCKS5.UsingPeerName}, {"Reachability", strings.ToLower(stats.Reachability)}, {"Uptime", stats.Uptime.Round(time.Second).String()}, {"Server version", stats.ServerVersion}, @@ -38,6 +37,13 @@ func printStatus(api *apiclient.Client) error { return nil } +func formatWorkingStatus(working bool) string { + if working { + return "working" + } + return "not working" +} + func printPeerId(api *apiclient.Client) error { info, err := api.PeerInfo() if err != nil { @@ -60,3 +66,33 @@ func renameMe(api *apiclient.Client, newName string) error { return nil } + +func listProxies(api *apiclient.Client) error { + proxies, err := api.ListAvailableProxies() + if err != nil { + return err + } + + if len(proxies) == 0 { + fmt.Println("no available proxies") + return nil + } + + fmt.Println("Proxies:") + for _, proxy := range proxies { + fmt.Printf("- peer name: %s | peer id: %s\n", proxy.PeerName, proxy.PeerID) + } + + return nil +} + +func setProxy(api *apiclient.Client, peerID string) error { + err := api.UpdateProxySettings(peerID) + if err != nil { + return err + } + + fmt.Println("proxy settings updated successfully") + + return nil +} diff --git a/cli/peers.go b/cli/peers.go index c5b5923..ab12b32 100644 --- a/cli/peers.go +++ b/cli/peers.go @@ -24,6 +24,7 @@ func printPeersStatus(api *apiclient.Client, format string) error { TableFormatNetworkUsage = "u" TableFormatConnection = "c" TableFormatVersion = "v" + TableFormatExitNode = "e" ) fHeaderMap := map[string]string{ @@ -35,6 +36,7 @@ func printPeersStatus(api *apiclient.Client, format string) error { TableFormatNetworkUsage: "network usage\n(↓in/↑out)", TableFormatConnection: "connections\naddress | protocol", TableFormatVersion: "version", + TableFormatExitNode: "exit node", } if len(format) < 1 { @@ -122,6 +124,8 @@ func printPeersStatus(api *apiclient.Client, format string) error { row = append(row, strings.Join(consStr, "\n")) case TableFormatVersion: row = append(row, peer.Version) + case TableFormatExitNode: + row = append(row, fmt.Sprintf("we allow: %v\npeer allowed: %v", peer.WeAllowUsingAsExitNode, peer.AllowedUsingAsExitNode)) } } table.Append(row)