diff --git a/Dockerfile b/Dockerfile index e7d72a42..bd2e976a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -240,6 +240,11 @@ ENV VPN_SERVICE_PROVIDER=pia \ SHADOWSOCKS_PASSWORD= \ SHADOWSOCKS_PASSWORD_SECRETFILE=/run/secrets/shadowsocks_password \ SHADOWSOCKS_CIPHER=chacha20-ietf-poly1305 \ + # Socks5 + SOCKS5_ENABLED=off \ + SOCKS5_LISTENING_ADDRESS=":1080" \ + SOCKS5_USER= \ + SOCKS5_PASSWORD= \ # Control server HTTP_CONTROL_SERVER_LOG=on \ HTTP_CONTROL_SERVER_ADDRESS=":8000" \ diff --git a/cmd/gluetun/main.go b/cmd/gluetun/main.go index 97209889..ebbf92da 100644 --- a/cmd/gluetun/main.go +++ b/cmd/gluetun/main.go @@ -41,6 +41,7 @@ import ( "github.com/qdm12/gluetun/internal/routing" "github.com/qdm12/gluetun/internal/server" "github.com/qdm12/gluetun/internal/shadowsocks" + "github.com/qdm12/gluetun/internal/socks5" "github.com/qdm12/gluetun/internal/storage" updater "github.com/qdm12/gluetun/internal/updater/loop" "github.com/qdm12/gluetun/internal/updater/resolver" @@ -411,6 +412,17 @@ func _main(ctx context.Context, buildInfo models.BuildInformation, return fmt.Errorf("starting public ip loop: %w", err) } + socks5Loop := socks5.NewLoop(socks5.Settings{ + Username: *allSettings.Socks5.Username, + Password: *allSettings.Socks5.Password, + Address: allSettings.Socks5.ListeningAddress, + Logger: logger.New(log.SetComponent("socks5")), + }) + socks5RunError, err := socks5Loop.Start(ctx) + if err != nil { + return fmt.Errorf("starting SOCKS5 server loop: %w", err) + } + healthLogger := logger.New(log.SetComponent("healthcheck")) healthcheckServer := healthcheck.NewServer(allSettings.Health, healthLogger) healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler( @@ -506,7 +518,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation, String() string Stop() error }{ - portForwardLooper, publicIPLooper, + portForwardLooper, publicIPLooper, socks5Loop, } for _, stopper := range stoppers { err := stopper.Stop() @@ -518,6 +530,8 @@ func _main(ctx context.Context, buildInfo models.BuildInformation, logger.Errorf("port forwarding loop crashed: %s", err) case err := <-publicIPRunError: logger.Errorf("public IP loop crashed: %s", err) + case err := <-socks5RunError: + logger.Errorf("SOCKS5 server loop crashed: %s", err) } return orderHandler.Shutdown(context.Background()) diff --git a/internal/configuration/settings/settings.go b/internal/configuration/settings/settings.go index 88e02669..f4529885 100644 --- a/internal/configuration/settings/settings.go +++ b/internal/configuration/settings/settings.go @@ -20,6 +20,7 @@ type Settings struct { HTTPProxy HTTPProxy Log Log PublicIP PublicIP + Socks5 Socks5 Shadowsocks Shadowsocks Storage Storage System System @@ -49,6 +50,7 @@ func (s *Settings) Validate(filterChoicesGetter FilterChoicesGetter, ipv6Support "http proxy": s.HTTPProxy.validate, "log": s.Log.validate, "public ip check": s.PublicIP.validate, + "socks5": s.Socks5.validate, "shadowsocks": s.Shadowsocks.validate, "storage": s.Storage.validate, "system": s.System.validate, @@ -81,6 +83,7 @@ func (s *Settings) copy() (copied Settings) { HTTPProxy: s.HTTPProxy.copy(), Log: s.Log.copy(), PublicIP: s.PublicIP.copy(), + Socks5: s.Socks5.copy(), Shadowsocks: s.Shadowsocks.copy(), Storage: s.Storage.copy(), System: s.System.copy(), @@ -104,6 +107,7 @@ func (s *Settings) OverrideWith(other Settings, patchedSettings.HTTPProxy.overrideWith(other.HTTPProxy) patchedSettings.Log.overrideWith(other.Log) patchedSettings.PublicIP.overrideWith(other.PublicIP) + patchedSettings.Socks5.overrideWith(other.Socks5) patchedSettings.Shadowsocks.overrideWith(other.Shadowsocks) patchedSettings.Storage.overrideWith(other.Storage) patchedSettings.System.overrideWith(other.System) @@ -131,6 +135,7 @@ func (s *Settings) SetDefaults() { s.Log.setDefaults() s.IPv6.setDefaults() s.PublicIP.setDefaults() + s.Socks5.setDefaults() s.Shadowsocks.setDefaults() s.Storage.SetDefaults() s.System.setDefaults() @@ -154,6 +159,7 @@ func (s Settings) toLinesNode() (node *gotree.Node) { node.AppendNode(s.Log.toLinesNode()) node.AppendNode(s.IPv6.toLinesNode()) node.AppendNode(s.Health.toLinesNode()) + node.AppendNode(s.Socks5.toLinesNode()) node.AppendNode(s.Shadowsocks.toLinesNode()) node.AppendNode(s.HTTPProxy.toLinesNode()) node.AppendNode(s.ControlServer.toLinesNode()) @@ -212,6 +218,7 @@ func (s *Settings) Read(r *reader.Reader, warner Warner) (err error) { "public ip": func(r *reader.Reader) error { return s.PublicIP.read(r, warner) }, + "socks5": s.Socks5.read, "shadowsocks": s.Shadowsocks.read, "storage": s.Storage.Read, "system": s.System.read, diff --git a/internal/configuration/settings/settings_test.go b/internal/configuration/settings/settings_test.go index 1fbb18a6..999516e3 100644 --- a/internal/configuration/settings/settings_test.go +++ b/internal/configuration/settings/settings_test.go @@ -81,6 +81,8 @@ func Test_Settings_String(t *testing.T) { | | ├── 1.1.1.1 | | └── 8.8.8.8 | └── Restart VPN on healthcheck failure: yes +├── SOCKS5 proxy settings: +| └── Enabled: no ├── Shadowsocks server settings: | └── Enabled: no ├── HTTP proxy settings: diff --git a/internal/configuration/settings/socks5.go b/internal/configuration/settings/socks5.go new file mode 100644 index 00000000..f9b0cdc5 --- /dev/null +++ b/internal/configuration/settings/socks5.go @@ -0,0 +1,83 @@ +package settings + +import ( + "fmt" + "os" + + "github.com/qdm12/gosettings" + "github.com/qdm12/gosettings/reader" + "github.com/qdm12/gosettings/validate" + "github.com/qdm12/gotree" +) + +// Socks5 contains settings to configure the Socks5 proxy server. +type Socks5 struct { + Enabled *bool + ListeningAddress string + Username *string + Password *string +} + +func (s Socks5) validate() (err error) { + err = validate.ListeningAddress(s.ListeningAddress, os.Getuid()) + if err != nil { + return fmt.Errorf("server listening address is not valid: %w", err) + } + + return nil +} + +func (s *Socks5) copy() (copied Socks5) { + return Socks5{ + Enabled: gosettings.CopyPointer(s.Enabled), + ListeningAddress: s.ListeningAddress, + Username: gosettings.CopyPointer(s.Username), + Password: gosettings.CopyPointer(s.Password), + } +} + +func (s *Socks5) overrideWith(other Socks5) { + s.Enabled = gosettings.OverrideWithPointer(s.Enabled, other.Enabled) + s.ListeningAddress = gosettings.OverrideWithComparable(s.ListeningAddress, other.ListeningAddress) + s.Username = gosettings.OverrideWithPointer(s.Username, other.Username) + s.Password = gosettings.OverrideWithPointer(s.Password, other.Password) +} + +func (s *Socks5) setDefaults() { + s.Enabled = gosettings.DefaultPointer(s.Enabled, false) + s.ListeningAddress = gosettings.DefaultComparable(s.ListeningAddress, ":1080") + s.Username = gosettings.DefaultPointer(s.Username, "") + s.Password = gosettings.DefaultPointer(s.Password, "") +} + +func (s Socks5) String() string { + return s.toLinesNode().String() +} + +func (s Socks5) toLinesNode() (node *gotree.Node) { + node = gotree.New("SOCKS5 proxy server settings:") + node.Appendf("Enabled: %s", gosettings.BoolToYesNo(s.Enabled)) + if !*s.Enabled { + return node + } + + node.Appendf("Listening address: %s", s.ListeningAddress) + if *s.Username != "" || *s.Password != "" { + node.Appendf("Username: %s", *s.Username) + node.Appendf("Password: %s", gosettings.ObfuscateKey(*s.Password)) + } + return node +} + +func (s *Socks5) read(r *reader.Reader) (err error) { + s.Enabled, err = r.BoolPtr("SOCKS5_ENABLED") + if err != nil { + return err + } + + s.ListeningAddress = r.String("SOCKS5_LISTENING_ADDRESS") + s.Username = r.Get("SOCKS5_USER", reader.ForceLowercase(false)) + s.Password = r.Get("SOCKS5_PASSWORD", reader.ForceLowercase(false)) + + return nil +} diff --git a/internal/socks5/loop.go b/internal/socks5/loop.go new file mode 100644 index 00000000..904cb570 --- /dev/null +++ b/internal/socks5/loop.go @@ -0,0 +1,85 @@ +package socks5 + +import ( + "context" + "sync" + "time" +) + +type Loop struct { + settings Settings + + mutex sync.Mutex + runCancel context.CancelFunc + runDone <-chan error +} + +func NewLoop(settings Settings) *Loop { + return &Loop{ + settings: settings, + } +} + +func (l *Loop) String() string { + return "SOCKS5 server loop" +} + +func (l *Loop) Start(_ context.Context) (runError <-chan error, err error) { + l.mutex.Lock() + defer l.mutex.Unlock() + + var runCtx context.Context + runCtx, l.runCancel = context.WithCancel(context.Background()) + + runDone := make(chan error) + l.runDone = runDone + runErrorCh := make(chan error) + + go run(runCtx, runDone, l.settings) + + return runErrorCh, nil +} + +func run(ctx context.Context, done chan<- error, settings Settings) { + defer close(done) + logger := settings.Logger + + for ctx.Err() == nil { + server := newServer(settings) + errorCh, err := server.Start(ctx) + if err != nil { + logger.Warnf("failed starting SOCKS5 server: %s", err) + waitBeforeRetry(ctx) + continue + } + + select { + case <-ctx.Done(): + done <- server.Stop() + return + case err := <-errorCh: + if ctx.Err() != nil { + return + } + logger.Warnf("SOCKS5 server crashed: %s", err) + waitBeforeRetry(ctx) + } + } +} + +func (l *Loop) Stop() (err error) { + l.mutex.Lock() + defer l.mutex.Unlock() + + l.runCancel() + return <-l.runDone +} + +func waitBeforeRetry(ctx context.Context) { + const retryDelay = 10 * time.Second + timer := time.NewTimer(retryDelay) + select { + case <-timer.C: + case <-ctx.Done(): + } +} diff --git a/internal/socks5/server.go b/internal/socks5/server.go index 3ea983d0..a6851400 100644 --- a/internal/socks5/server.go +++ b/internal/socks5/server.go @@ -23,7 +23,7 @@ type Server struct { stopping atomic.Bool } -func New(settings Settings) *Server { +func newServer(settings Settings) *Server { return &Server{ username: settings.Username, password: settings.Password, diff --git a/internal/socks5/socks5_test.go b/internal/socks5/socks5_test.go index 87f6c2b9..4545e7d9 100644 --- a/internal/socks5/socks5_test.go +++ b/internal/socks5/socks5_test.go @@ -50,7 +50,7 @@ func TestServerProxy(t *testing.T) { backendConnCh <- conn }() - server := New(Settings{ + server := newServer(Settings{ Username: testCase.username, Password: testCase.password, Address: "127.0.0.1:0", @@ -215,7 +215,7 @@ func TestNew(t *testing.T) { for name, testCase := range testCases { t.Run(name, func(t *testing.T) { t.Parallel() - result := New(testCase.settings) + result := newServer(testCase.settings) assert.Equal(t, testCase.expected.username, result.username) assert.Equal(t, testCase.expected.password, result.password) assert.Equal(t, testCase.expected.address, result.address) @@ -231,7 +231,7 @@ func TestStartStop(t *testing.T) { logger.EXPECT().Infof(gomock.Any(), gomock.Any()).Times(0) logger.EXPECT().Warnf(gomock.Any(), gomock.Any()).Times(0) - server := New(Settings{ + server := newServer(Settings{ Address: "127.0.0.1:0", Logger: logger, })