Skip to content
Open
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
Add socks5 functionality
  • Loading branch information
angelakis committed Feb 11, 2026
commit f04fa93be64182fd4320f69ac7b9c132612448d5
10 changes: 9 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,14 @@ ENV VPN_SERVICE_PROVIDER=pia \
HTTPPROXY_PASSWORD= \
HTTPPROXY_USER_SECRETFILE=/run/secrets/httpproxy_user \
HTTPPROXY_PASSWORD_SECRETFILE=/run/secrets/httpproxy_password \
# SOCKS5
SOCKS5=off \
SOCKS5_LOG=off \
SOCKS5_LISTENING_ADDRESS=":1080" \
SOCKS5_USER= \
SOCKS5_PASSWORD= \
SOCKS5_USER_SECRETFILE=/run/secrets/socks5_user \
SOCKS5_PASSWORD_SECRETFILE=/run/secrets/socks5_password \
# Shadowsocks
SHADOWSOCKS=off \
SHADOWSOCKS_LOG=off \
Expand Down Expand Up @@ -232,7 +240,7 @@ ENV VPN_SERVICE_PROVIDER=pia \
PUID=1000 \
PGID=1000
ENTRYPOINT ["/gluetun-entrypoint"]
EXPOSE 8000/tcp 8888/tcp 8388/tcp 8388/udp
EXPOSE 8000/tcp 8888/tcp 1080/tcp 8388/tcp 8388/udp
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=3 CMD /gluetun-entrypoint healthcheck
ARG TARGETPLATFORM
RUN apk add --no-cache --update -l wget && \
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ Lightweight swiss-army-knife-like VPN client to multiple VPN service providers
- Built in firewall kill switch to allow traffic only with needed the VPN servers and LAN devices
- Built in Shadowsocks proxy server (protocol based on SOCKS5 with an encryption layer, tunnels TCP+UDP)
- Built in HTTP proxy (tunnels HTTP and HTTPS through TCP)
- Built in SOCKS5 proxy (tunnels TCP)
- [Connect other containers to it](https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-container-to-gluetun.md)
- [Connect LAN devices to it](https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-lan-device-to-gluetun.md)
- Compatible with amd64, i686 (32 bit), **ARM** 64 bit, ARM 32 bit v6 and v7, and even ppc64le 🎆
Expand Down Expand Up @@ -102,6 +103,7 @@ services:
- /dev/net/tun:/dev/net/tun
ports:
- 8888:8888/tcp # HTTP proxy
- 1080:1080/tcp # SOCKS5
- 8388:8388/tcp # Shadowsocks
- 8388:8388/udp # Shadowsocks
volumes:
Expand Down
8 changes: 8 additions & 0 deletions cmd/gluetun/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import (
"github.com/qdm12/gluetun/internal/publicip"
"github.com/qdm12/gluetun/internal/routing"
"github.com/qdm12/gluetun/internal/server"
"github.com/qdm12/gluetun/internal/socks5"
"github.com/qdm12/gluetun/internal/shadowsocks"
"github.com/qdm12/gluetun/internal/storage"
"github.com/qdm12/gluetun/internal/tun"
Expand Down Expand Up @@ -474,6 +475,13 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
go shadowsocksLooper.Run(shadowsocksCtx, shadowsocksDone)
otherGroupHandler.Add(shadowsocksHandler)

socks5Looper := socks5.NewLoop(allSettings.Socks5,
logger.New(log.SetComponent("socks5")))
socks5Handler, socks5Ctx, socks5Done := goshutdown.NewGoRoutineHandler(
"socks5 proxy", goroutine.OptionTimeout(defaultShutdownTimeout))
go socks5Looper.Run(socks5Ctx, socks5Done)
otherGroupHandler.Add(socks5Handler)

httpServerHandler, httpServerCtx, httpServerDone := goshutdown.NewGoRoutineHandler(
"http server", goroutine.OptionTimeout(defaultShutdownTimeout))
httpServer, err := server.New(httpServerCtx, allSettings.ControlServer,
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.25.0

require (
github.com/ProtonMail/go-srp v0.0.7
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
github.com/breml/rootcerts v0.3.3
github.com/fatih/color v1.18.0
github.com/golang/mock v1.6.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ github.com/ProtonMail/go-crypto v1.3.0-proton h1:tAQKQRZX/73VmzK6yHSCaRUOvS/3OYS
github.com/ProtonMail/go-crypto v1.3.0-proton/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/breml/rootcerts v0.3.3 h1://GnaRtQ/9BY2+GtMk2wtWxVdCRysiaPr5/xBwl7NKw=
Expand Down
7 changes: 7 additions & 0 deletions internal/configuration/settings/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type Settings struct {
Log Log
PublicIP PublicIP
Shadowsocks Shadowsocks
Socks5 Socks5
Storage Storage
System System
Updater Updater
Expand Down Expand Up @@ -49,6 +50,7 @@ func (s *Settings) Validate(filterChoicesGetter FilterChoicesGetter, ipv6Support
"log": s.Log.validate,
"public ip check": s.PublicIP.validate,
"shadowsocks": s.Shadowsocks.validate,
"socks5": s.Socks5.validate,
"storage": s.Storage.validate,
"system": s.System.validate,
"updater": s.Updater.Validate,
Expand Down Expand Up @@ -79,6 +81,7 @@ func (s *Settings) copy() (copied Settings) {
Log: s.Log.copy(),
PublicIP: s.PublicIP.copy(),
Shadowsocks: s.Shadowsocks.copy(),
Socks5: s.Socks5.copy(),
Storage: s.Storage.copy(),
System: s.System.copy(),
Updater: s.Updater.copy(),
Expand All @@ -100,6 +103,7 @@ func (s *Settings) OverrideWith(other Settings,
patchedSettings.Log.overrideWith(other.Log)
patchedSettings.PublicIP.overrideWith(other.PublicIP)
patchedSettings.Shadowsocks.overrideWith(other.Shadowsocks)
patchedSettings.Socks5.overrideWith(other.Socks5)
patchedSettings.Storage.overrideWith(other.Storage)
patchedSettings.System.overrideWith(other.System)
patchedSettings.Updater.overrideWith(other.Updater)
Expand All @@ -123,6 +127,7 @@ func (s *Settings) SetDefaults() {
s.Log.setDefaults()
s.PublicIP.setDefaults()
s.Shadowsocks.setDefaults()
s.Socks5.setDefaults()
s.Storage.setDefaults()
s.System.setDefaults()
s.Version.setDefaults()
Expand All @@ -144,6 +149,7 @@ func (s Settings) toLinesNode() (node *gotree.Node) {
node.AppendNode(s.Log.toLinesNode())
node.AppendNode(s.Health.toLinesNode())
node.AppendNode(s.Shadowsocks.toLinesNode())
node.AppendNode(s.Socks5.toLinesNode())
node.AppendNode(s.HTTPProxy.toLinesNode())
node.AppendNode(s.ControlServer.toLinesNode())
node.AppendNode(s.Storage.toLinesNode())
Expand Down Expand Up @@ -203,6 +209,7 @@ func (s *Settings) Read(r *reader.Reader, warner Warner) (err error) {
return s.PublicIP.read(r, warner)
},
"shadowsocks": s.Shadowsocks.read,
"socks5": s.Socks5.read,
"storage": s.Storage.read,
"system": s.System.read,
"updater": s.Updater.read,
Expand Down
2 changes: 2 additions & 0 deletions internal/configuration/settings/settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ func Test_Settings_String(t *testing.T) {
| └── Restart VPN on healthcheck failure: yes
├── Shadowsocks server settings:
| └── Enabled: no
├── SOCKS5 server settings:
| └── Enabled: no
├── HTTP proxy settings:
| └── Enabled: no
├── Control server settings:
Expand Down
100 changes: 100 additions & 0 deletions internal/configuration/settings/socks5.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
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 server.
type Socks5 struct {
// Enabled is true if the server should be running.
// It defaults to false, and cannot be nil in the internal state.
Enabled *bool
ListeningAddress string
User *string
Password *string
Log *bool
}

func (s Socks5) validate() (err error) {
err = validate.ListeningAddress(s.ListeningAddress, os.Getuid())
if err != nil {
return fmt.Errorf("%w: %s", ErrServerAddressNotValid, s.ListeningAddress)
}
return nil
}

func (s *Socks5) copy() (copied Socks5) {
return Socks5{
Enabled: gosettings.CopyPointer(s.Enabled),
ListeningAddress: s.ListeningAddress,
User: gosettings.CopyPointer(s.User),
Password: gosettings.CopyPointer(s.Password),
Log: gosettings.CopyPointer(s.Log),
}
}

// overrideWith overrides fields of the receiver
// settings object with any field set in the other
// settings.
func (s *Socks5) overrideWith(other Socks5) {
s.Enabled = gosettings.OverrideWithPointer(s.Enabled, other.Enabled)
s.ListeningAddress = gosettings.OverrideWithComparable(s.ListeningAddress, other.ListeningAddress)
s.User = gosettings.OverrideWithPointer(s.User, other.User)
s.Password = gosettings.OverrideWithPointer(s.Password, other.Password)
s.Log = gosettings.OverrideWithPointer(s.Log, other.Log)
}

func (s *Socks5) setDefaults() {
s.Enabled = gosettings.DefaultPointer(s.Enabled, false)
s.ListeningAddress = gosettings.DefaultComparable(s.ListeningAddress, ":1080")
s.User = gosettings.DefaultPointer(s.User, "")
s.Password = gosettings.DefaultPointer(s.Password, "")
s.Log = gosettings.DefaultPointer(s.Log, false)
}

func (s Socks5) String() string {
return s.toLinesNode().String()
}

func (s Socks5) toLinesNode() (node *gotree.Node) {
node = gotree.New("SOCKS5 server settings:")

node.Appendf("Enabled: %s", gosettings.BoolToYesNo(s.Enabled))
if !*s.Enabled {
return node
}

node.Appendf("Listening address: %s", s.ListeningAddress)
node.Appendf("User: %s", *s.User)
node.Appendf("Password: %s", gosettings.ObfuscateKey(*s.Password))
node.Appendf("Log: %s", gosettings.BoolToYesNo(s.Log))

return node
}

func (s *Socks5) read(r *reader.Reader) (err error) {
s.Enabled, err = r.BoolPtr("SOCKS5")
if err != nil {
return err
}

s.ListeningAddress = r.String("SOCKS5_LISTENING_ADDRESS")

s.User = r.Get("SOCKS5_USER",
reader.ForceLowercase(false))
s.Password = r.Get("SOCKS5_PASSWORD",
reader.ForceLowercase(false))

s.Log, err = r.BoolPtr("SOCKS5_LOG")
if err != nil {
return err
}

return nil
}
19 changes: 19 additions & 0 deletions internal/socks5/logger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package socks5

type Logger interface {
debuger
infoer
errorer
}

type debuger interface {
Debug(s string)
}

type infoer interface {
Info(s string)
}

type errorer interface {
Error(s string)
}
Loading