Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions application.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ func (a *Application) Init(ctx context.Context, tunDevice tun.Device) error {
p2pHost.SetStreamHandler(protocol.TunnelPacketMethod, a.Tunnel.StreamHandler)
}
p2pHost.SetStreamHandler(protocol.Socks5PacketMethod, a.SOCKS5.ProxyStreamHandler)
p2pHost.SetStreamHandler(protocol.Socks5NoAuthMethod, a.SOCKS5.ProxyStreamHandler)

if a.Tunnel != nil {
awlevent.WrapSubscriptionToCallback(a.ctx, func(_ interface{}) {
Expand Down
10 changes: 6 additions & 4 deletions application_simnet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import (
"github.com/anywherelan/awl/vpn"
)

const EnvRunPerfTests = "AWL_PERF_TESTS"

/*
TestSimulatedTunnelPerformance performs a benchmark of the AWL VPN tunnel under simulated network conditions.

Expand All @@ -32,8 +34,8 @@ It uses:
func TestSimulatedTunnelPerformance(t *testing.T) {
const Mbps = 1_000_000

if os.Getenv("CI") != "" {
t.Skip("skip in CI because it's a benchmark")
if os.Getenv(EnvRunPerfTests) == "" {
t.Skipf("skip perf test because %s env is empty", EnvRunPerfTests)
}

scenarios := []struct {
Expand Down Expand Up @@ -230,8 +232,8 @@ func TestSimulatedSOCKS5ProxyPerformance(t *testing.T) {

const Mbps = 1_000_000

if os.Getenv("CI") != "" {
t.Skip("skip in CI because it's a benchmark")
if os.Getenv(EnvRunPerfTests) == "" {
t.Skipf("skip perf test because %s env is empty", EnvRunPerfTests)
}

scenarios := []struct {
Expand Down
38 changes: 38 additions & 0 deletions application_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/anywherelan/awl/api"
"github.com/anywherelan/awl/config"
"github.com/anywherelan/awl/entity"
"github.com/anywherelan/awl/protocol"
)

func TestMakeFriends(t *testing.T) {
Expand Down Expand Up @@ -391,6 +392,43 @@ func TestUpdateUseAsExitNodeConfig(t *testing.T) {
ts.Equal("", info.SOCKS5.UsingPeerID)
}

func TestSOCKS5ProxyFallbackToOldProtocol(t *testing.T) {
ts := NewTestSuite(t)

peer1 := ts.NewTestPeer(false) // client
peer2 := ts.NewTestPeer(false) // server (simulates old version)

ts.makeFriends(peer2, peer1)

// Remove new protocol handler from peer2 to simulate old peer
peer2.app.P2p.Host().RemoveStreamHandler(protocol.Socks5NoAuthMethod)

// Allow peer1 to use peer2 as exit node
peer1Config, err := peer2.api.KnownPeerConfig(peer1.PeerID())
ts.NoError(err)

err = peer2.api.UpdatePeerSettings(entity.UpdatePeerSettingsRequest{
PeerID: peer1.PeerID(),
Alias: peer1Config.Alias,
DomainName: peer1Config.DomainName,
IPAddr: peer1Config.IPAddr,
AllowUsingAsExitNode: true,
})
ts.NoError(err)

ts.Eventually(func() bool {
peer2Config, err := peer1.api.KnownPeerConfig(peer2.PeerID())
ts.NoError(err)
return peer2Config.AllowedUsingAsExitNode
}, 15*time.Second, 100*time.Millisecond)

peer1.app.SOCKS5.SetProxyPeerID(peer2.PeerID())
peer2.app.SOCKS5.SetProxyingLocalhostEnabled(true)

// Verify SOCKS5 still works via fallback to old protocol
testSOCKS5Proxy(ts, peer1.app.Conf.SOCKS5.ListenAddress, "")
}

func TestUpdatePeerSettingsIPAddr(t *testing.T) {
ts := NewTestSuite(t)

Expand Down
2 changes: 1 addition & 1 deletion cmd/awl-tray/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.25.0

replace (
github.com/anywherelan/awl => ../../
github.com/haxii/socks5 => github.com/anywherelan/socks5 v0.0.0-20260112065346-bea9abfe9bdc
github.com/haxii/socks5 => github.com/anywherelan/socks5 v0.0.0-20260404130029-d414a53712fc
github.com/ipfs/go-log/v2 => github.com/anywherelan/go-log/v2 v2.0.3-0.20221101180049-46e3967f6fe5
github.com/ncruces/zenity => github.com/pymq/zenity v0.0.0-20230509161854-c117c448544d
)
Expand Down
4 changes: 2 additions & 2 deletions cmd/awl-tray/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ github.com/akavel/rsrc v0.10.2 h1:Zxm8V5eI1hW4gGaYsJQUhxpjkENuG91ki8B4zCrvEsw=
github.com/akavel/rsrc v0.10.2/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/anywherelan/go-log/v2 v2.0.3-0.20221101180049-46e3967f6fe5 h1:uQsw+HnQo6Ru5eFgUdEQYKMwkiYkoNZDQmY2ob1E58Y=
github.com/anywherelan/go-log/v2 v2.0.3-0.20221101180049-46e3967f6fe5/go.mod h1:r8UEDyeHO6bYVcP9R2/HnK2ZSZ5CJp89gubcHLKfRv0=
github.com/anywherelan/socks5 v0.0.0-20260112065346-bea9abfe9bdc h1:BxUih+wVbxEfAAC4+vzXSoDXHNFLgnLR9AmeXRtJbuk=
github.com/anywherelan/socks5 v0.0.0-20260112065346-bea9abfe9bdc/go.mod h1:KpbKhNH2RqojJGPVT9faDXnYWkqzfgpKeehC8xTKaeY=
github.com/anywherelan/socks5 v0.0.0-20260404130029-d414a53712fc h1:icO+nO4NZtJLZS8JnrCpGk8GGaaKNeleuY7WH7qzVik=
github.com/anywherelan/socks5 v0.0.0-20260404130029-d414a53712fc/go.mod h1:KpbKhNH2RqojJGPVT9faDXnYWkqzfgpKeehC8xTKaeY=
github.com/anywherelan/ts-dns v0.0.0-20240721135326-6d6b7b811853 h1:RVKWGnppAfxgD2wphkq+OYDOqqI8zgbymBLl2pxYKzY=
github.com/anywherelan/ts-dns v0.0.0-20240721135326-6d6b7b811853/go.mod h1:ly7HpPle1G3D0jwrr12uolTGWKN3DPgxzBYNR086BLo=
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/anywherelan/awl
go 1.25.0

replace (
github.com/haxii/socks5 => github.com/anywherelan/socks5 v0.0.0-20260112065346-bea9abfe9bdc
github.com/haxii/socks5 => github.com/anywherelan/socks5 v0.0.0-20260404130029-d414a53712fc
github.com/ipfs/go-log/v2 => github.com/anywherelan/go-log/v2 v2.0.3-0.20221101180049-46e3967f6fe5
)

Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ github.com/GrigoryKrasnochub/updaterini v0.1.0 h1:csl3bqQIShuI8RnXzKprTgEhqi3I59
github.com/GrigoryKrasnochub/updaterini v0.1.0/go.mod h1:w+8blQZCZzGmJGXqmXbeSOsUOF6LqyeqckxSiZah7RI=
github.com/anywherelan/go-log/v2 v2.0.3-0.20221101180049-46e3967f6fe5 h1:uQsw+HnQo6Ru5eFgUdEQYKMwkiYkoNZDQmY2ob1E58Y=
github.com/anywherelan/go-log/v2 v2.0.3-0.20221101180049-46e3967f6fe5/go.mod h1:r8UEDyeHO6bYVcP9R2/HnK2ZSZ5CJp89gubcHLKfRv0=
github.com/anywherelan/socks5 v0.0.0-20260112065346-bea9abfe9bdc h1:BxUih+wVbxEfAAC4+vzXSoDXHNFLgnLR9AmeXRtJbuk=
github.com/anywherelan/socks5 v0.0.0-20260112065346-bea9abfe9bdc/go.mod h1:KpbKhNH2RqojJGPVT9faDXnYWkqzfgpKeehC8xTKaeY=
github.com/anywherelan/socks5 v0.0.0-20260404130029-d414a53712fc h1:icO+nO4NZtJLZS8JnrCpGk8GGaaKNeleuY7WH7qzVik=
github.com/anywherelan/socks5 v0.0.0-20260404130029-d414a53712fc/go.mod h1:KpbKhNH2RqojJGPVT9faDXnYWkqzfgpKeehC8xTKaeY=
github.com/anywherelan/ts-dns v0.0.0-20240721135326-6d6b7b811853 h1:RVKWGnppAfxgD2wphkq+OYDOqqI8zgbymBLl2pxYKzY=
github.com/anywherelan/ts-dns v0.0.0-20240721135326-6d6b7b811853/go.mod h1:ly7HpPle1G3D0jwrr12uolTGWKN3DPgxzBYNR086BLo=
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
Expand Down
5 changes: 5 additions & 0 deletions p2p/p2p.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,11 @@ func (p *P2p) NewStream(ctx context.Context, id peer.ID, proto protocol.ID) (net
return p.host.NewStream(ctx, id, proto)
}

func (p *P2p) NewStreamMulti(ctx context.Context, id peer.ID, protos ...protocol.ID) (network.Stream, error) {
ctx = network.WithAllowLimitedConn(ctx, "awl")
return p.host.NewStream(ctx, id, protos...)
}

func (p *P2p) NewStreamWithDedicatedConn(ctx context.Context, id peer.ID, proto protocol.ID) (network.Stream, error) {
ctx = network.WithAllowLimitedConn(ctx, "awl")

Expand Down
1 change: 1 addition & 0 deletions protocol/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const (
GetStatusMethod protocol.ID = basePath + "/status/"
TunnelPacketMethod protocol.ID = basePath + "/tunnel/"
Socks5PacketMethod protocol.ID = basePath + "/socks5/"
Socks5NoAuthMethod protocol.ID = basePath + "/socks5-noauth/"
)

type (
Expand Down
1 change: 1 addition & 0 deletions service/auth_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ 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)
NewStreamMulti(ctx context.Context, id peer.ID, protos ...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))
RecordPeerLatency(id peer.ID, rtt time.Duration)
Expand Down
18 changes: 15 additions & 3 deletions service/socks5.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,18 @@ func (s *SOCKS5) ProxyStreamHandler(stream network.Stream) {

if !enabled {
metrics.SOCKS5ErrorsTotal.WithLabelValues("server", "proxying_disabled").Inc()
_ = s.server.SendServerFailureReply(stream)
if stream.Protocol() != protocol.Socks5NoAuthMethod {
_ = s.server.SendServerFailureReply(stream)
}
return
}

// ignore error, we can do nothing about it
_ = s.server.ServeStreamConn(stream)
if stream.Protocol() == protocol.Socks5NoAuthMethod {
_ = s.server.ServeStreamConnNoAuth(stream)
} else {
_ = s.server.ServeStreamConn(stream)
}

// stream.Write() + stream.Reset() are not guaranteed to run sequentially
// e.g reader on the other side may not read everything we sent because of stream.Reset()
Expand Down Expand Up @@ -201,7 +207,7 @@ func (s *SOCKS5) proxyConn(ctx context.Context, conn net.Conn) error {
return err
}

stream, err := s.p2p.NewStream(ctx, remotePeerID, protocol.Socks5PacketMethod)
stream, err := s.p2p.NewStreamMulti(ctx, remotePeerID, protocol.Socks5NoAuthMethod, protocol.Socks5PacketMethod)
if err != nil {
metrics.SOCKS5ErrorsTotal.WithLabelValues("client", "peer_stream_failed").Inc()
return err
Expand All @@ -210,6 +216,12 @@ func (s *SOCKS5) proxyConn(ctx context.Context, conn net.Conn) error {
_ = stream.Reset()
}()

if stream.Protocol() == protocol.Socks5NoAuthMethod {
if err := s.client.HandleLocalAuth(conn); err != nil {
return err
}
}

s.handleStream(conn, stream)

// stream.Write() + stream.Reset() are not guaranteed to run sequentially
Expand Down
38 changes: 38 additions & 0 deletions socks5/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package socks5

import (
"errors"
"fmt"
"net"
"time"

socks5Lib "github.com/haxii/socks5"
"github.com/ipfs/go-log/v2"
)

Expand Down Expand Up @@ -46,6 +48,42 @@ func (c *Client) ConnsChan() <-chan net.Conn {
return c.connsCh
}

// HandleLocalAuth performs the SOCKS5 auth negotiation locally, responding with NoAuth.
// This avoids sending the auth handshake over the network to the remote peer.
func (c *Client) HandleLocalAuth(conn net.Conn) error {
// Read version byte
version := []byte{0}
if _, err := conn.Read(version); err != nil {
return fmt.Errorf("failed to read version: %w", err)
}
if version[0] != 0x05 {
return fmt.Errorf("unsupported SOCKS version: %d", version[0])
}

// Read offered auth methods
methods, err := socks5Lib.ReadMethods(conn)
if err != nil {
return fmt.Errorf("failed to read auth methods: %w", err)
}

// Check NoAuth is offered
hasNoAuth := false
for _, m := range methods {
if m == socks5Lib.AuthMethodNoAuth {
hasNoAuth = true
break
}
}
if !hasNoAuth {
_, _ = conn.Write([]byte{0x05, socks5Lib.AuthMethodNoAcceptable})
return fmt.Errorf("client does not support NoAuth method")
}

// Respond: NoAuth selected
_, err = conn.Write([]byte{0x05, socks5Lib.AuthMethodNoAuth})
return err
}

func (c *Client) serve() error {
defer close(c.connsCh)

Expand Down
5 changes: 5 additions & 0 deletions socks5/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ func (s *Server) ServeStreamConn(stream network.Stream) error {
return s.socks.ServeConn(conn)
}

func (s *Server) ServeStreamConnNoAuth(stream network.Stream) error {
conn := StreamConnWrapper{Stream: stream}
return s.socks.ServeConnNoAuth(conn)
}

// ServeConn is only used in tests. TODO: refactor tests
func (s *Server) ServeConn(ioConn io.ReadWriteCloser) error {
conn := ReadWriterConnWrapper{ReadWriteCloser: ioConn}
Expand Down
Loading