Skip to content

Commit eb9916f

Browse files
authored
feat: socks5 proxy server (#3336)
- `SOCKS5_ENABLED=off` - `SOCKS5_LISTENING_ADDRESS=":1080"` - `SOCKS5_USER=` - `SOCKS5_PASSWORD=`
1 parent 2210a0e commit eb9916f

16 files changed

Lines changed: 1605 additions & 2 deletions

Dockerfile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,11 @@ ENV VPN_SERVICE_PROVIDER=pia \
240240
SHADOWSOCKS_PASSWORD= \
241241
SHADOWSOCKS_PASSWORD_SECRETFILE=/run/secrets/shadowsocks_password \
242242
SHADOWSOCKS_CIPHER=chacha20-ietf-poly1305 \
243+
# Socks5
244+
SOCKS5_ENABLED=off \
245+
SOCKS5_LISTENING_ADDRESS=":1080" \
246+
SOCKS5_USER= \
247+
SOCKS5_PASSWORD= \
243248
# Control server
244249
HTTP_CONTROL_SERVER_LOG=on \
245250
HTTP_CONTROL_SERVER_ADDRESS=":8000" \
@@ -271,7 +276,7 @@ ENV VPN_SERVICE_PROVIDER=pia \
271276
PUID=1000 \
272277
PGID=1000
273278
ENTRYPOINT ["/gluetun-entrypoint"]
274-
EXPOSE 8000/tcp 8888/tcp 8388/tcp 8388/udp
279+
EXPOSE 8000/tcp 8888/tcp 8388/tcp 8388/udp 1080/tcp
275280
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=3 CMD /gluetun-entrypoint healthcheck
276281
ARG TARGETPLATFORM
277282
RUN apk add --no-cache --update -l wget && \

cmd/gluetun/main.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import (
4141
"github.com/qdm12/gluetun/internal/routing"
4242
"github.com/qdm12/gluetun/internal/server"
4343
"github.com/qdm12/gluetun/internal/shadowsocks"
44+
"github.com/qdm12/gluetun/internal/socks5"
4445
"github.com/qdm12/gluetun/internal/storage"
4546
updater "github.com/qdm12/gluetun/internal/updater/loop"
4647
"github.com/qdm12/gluetun/internal/updater/resolver"
@@ -411,6 +412,18 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
411412
return fmt.Errorf("starting public ip loop: %w", err)
412413
}
413414

415+
socks5Loop := socks5.NewLoop(socks5.Settings{
416+
Enabled: *allSettings.Socks5.Enabled,
417+
Username: *allSettings.Socks5.Username,
418+
Password: *allSettings.Socks5.Password,
419+
Address: allSettings.Socks5.ListeningAddress,
420+
Logger: logger.New(log.SetComponent("socks5")),
421+
})
422+
socks5RunError, err := socks5Loop.Start(ctx)
423+
if err != nil {
424+
return fmt.Errorf("starting SOCKS5 server loop: %w", err)
425+
}
426+
414427
healthLogger := logger.New(log.SetComponent("healthcheck"))
415428
healthcheckServer := healthcheck.NewServer(allSettings.Health, healthLogger)
416429
healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler(
@@ -506,7 +519,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
506519
String() string
507520
Stop() error
508521
}{
509-
portForwardLooper, publicIPLooper,
522+
portForwardLooper, publicIPLooper, socks5Loop,
510523
}
511524
for _, stopper := range stoppers {
512525
err := stopper.Stop()
@@ -518,6 +531,8 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
518531
logger.Errorf("port forwarding loop crashed: %s", err)
519532
case err := <-publicIPRunError:
520533
logger.Errorf("public IP loop crashed: %s", err)
534+
case err := <-socks5RunError:
535+
logger.Errorf("SOCKS5 server loop crashed: %s", err)
521536
}
522537

523538
return orderHandler.Shutdown(context.Background())

internal/configuration/settings/settings.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type Settings struct {
2020
HTTPProxy HTTPProxy
2121
Log Log
2222
PublicIP PublicIP
23+
Socks5 Socks5
2324
Shadowsocks Shadowsocks
2425
Storage Storage
2526
System System
@@ -49,6 +50,7 @@ func (s *Settings) Validate(filterChoicesGetter FilterChoicesGetter, ipv6Support
4950
"http proxy": s.HTTPProxy.validate,
5051
"log": s.Log.validate,
5152
"public ip check": s.PublicIP.validate,
53+
"socks5": s.Socks5.validate,
5254
"shadowsocks": s.Shadowsocks.validate,
5355
"storage": s.Storage.validate,
5456
"system": s.System.validate,
@@ -81,6 +83,7 @@ func (s *Settings) copy() (copied Settings) {
8183
HTTPProxy: s.HTTPProxy.copy(),
8284
Log: s.Log.copy(),
8385
PublicIP: s.PublicIP.copy(),
86+
Socks5: s.Socks5.copy(),
8487
Shadowsocks: s.Shadowsocks.copy(),
8588
Storage: s.Storage.copy(),
8689
System: s.System.copy(),
@@ -104,6 +107,7 @@ func (s *Settings) OverrideWith(other Settings,
104107
patchedSettings.HTTPProxy.overrideWith(other.HTTPProxy)
105108
patchedSettings.Log.overrideWith(other.Log)
106109
patchedSettings.PublicIP.overrideWith(other.PublicIP)
110+
patchedSettings.Socks5.overrideWith(other.Socks5)
107111
patchedSettings.Shadowsocks.overrideWith(other.Shadowsocks)
108112
patchedSettings.Storage.overrideWith(other.Storage)
109113
patchedSettings.System.overrideWith(other.System)
@@ -131,6 +135,7 @@ func (s *Settings) SetDefaults() {
131135
s.Log.setDefaults()
132136
s.IPv6.setDefaults()
133137
s.PublicIP.setDefaults()
138+
s.Socks5.setDefaults()
134139
s.Shadowsocks.setDefaults()
135140
s.Storage.SetDefaults()
136141
s.System.setDefaults()
@@ -154,6 +159,7 @@ func (s Settings) toLinesNode() (node *gotree.Node) {
154159
node.AppendNode(s.Log.toLinesNode())
155160
node.AppendNode(s.IPv6.toLinesNode())
156161
node.AppendNode(s.Health.toLinesNode())
162+
node.AppendNode(s.Socks5.toLinesNode())
157163
node.AppendNode(s.Shadowsocks.toLinesNode())
158164
node.AppendNode(s.HTTPProxy.toLinesNode())
159165
node.AppendNode(s.ControlServer.toLinesNode())
@@ -212,6 +218,7 @@ func (s *Settings) Read(r *reader.Reader, warner Warner) (err error) {
212218
"public ip": func(r *reader.Reader) error {
213219
return s.PublicIP.read(r, warner)
214220
},
221+
"socks5": s.Socks5.read,
215222
"shadowsocks": s.Shadowsocks.read,
216223
"storage": s.Storage.Read,
217224
"system": s.System.read,

internal/configuration/settings/settings_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ func Test_Settings_String(t *testing.T) {
8181
| | ├── 1.1.1.1
8282
| | └── 8.8.8.8
8383
| └── Restart VPN on healthcheck failure: yes
84+
├── SOCKS5 proxy server settings:
85+
| └── Enabled: no
8486
├── Shadowsocks server settings:
8587
| └── Enabled: no
8688
├── HTTP proxy settings:
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package settings
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"os"
7+
8+
"github.com/qdm12/gosettings"
9+
"github.com/qdm12/gosettings/reader"
10+
"github.com/qdm12/gosettings/validate"
11+
"github.com/qdm12/gotree"
12+
)
13+
14+
// Socks5 contains settings to configure the Socks5 proxy server.
15+
type Socks5 struct {
16+
Enabled *bool
17+
ListeningAddress string
18+
Username *string
19+
Password *string
20+
}
21+
22+
func (s Socks5) validate() (err error) {
23+
err = validate.ListeningAddress(s.ListeningAddress, os.Getuid())
24+
if err != nil {
25+
return fmt.Errorf("server listening address is not valid: %w", err)
26+
}
27+
28+
switch {
29+
case *s.Username != "" && *s.Password == "":
30+
return errors.New("password must be set if username is set")
31+
case *s.Username == "" && *s.Password != "":
32+
return errors.New("username must be set if password is set")
33+
}
34+
35+
return nil
36+
}
37+
38+
func (s *Socks5) copy() (copied Socks5) {
39+
return Socks5{
40+
Enabled: gosettings.CopyPointer(s.Enabled),
41+
ListeningAddress: s.ListeningAddress,
42+
Username: gosettings.CopyPointer(s.Username),
43+
Password: gosettings.CopyPointer(s.Password),
44+
}
45+
}
46+
47+
func (s *Socks5) overrideWith(other Socks5) {
48+
s.Enabled = gosettings.OverrideWithPointer(s.Enabled, other.Enabled)
49+
s.ListeningAddress = gosettings.OverrideWithComparable(s.ListeningAddress, other.ListeningAddress)
50+
s.Username = gosettings.OverrideWithPointer(s.Username, other.Username)
51+
s.Password = gosettings.OverrideWithPointer(s.Password, other.Password)
52+
}
53+
54+
func (s *Socks5) setDefaults() {
55+
s.Enabled = gosettings.DefaultPointer(s.Enabled, false)
56+
s.ListeningAddress = gosettings.DefaultComparable(s.ListeningAddress, ":1080")
57+
s.Username = gosettings.DefaultPointer(s.Username, "")
58+
s.Password = gosettings.DefaultPointer(s.Password, "")
59+
}
60+
61+
func (s Socks5) String() string {
62+
return s.toLinesNode().String()
63+
}
64+
65+
func (s Socks5) toLinesNode() (node *gotree.Node) {
66+
node = gotree.New("SOCKS5 proxy server settings:")
67+
node.Appendf("Enabled: %s", gosettings.BoolToYesNo(s.Enabled))
68+
if !*s.Enabled {
69+
return node
70+
}
71+
72+
node.Appendf("Listening address: %s", s.ListeningAddress)
73+
if *s.Username != "" || *s.Password != "" {
74+
node.Appendf("Username: %s", *s.Username)
75+
node.Appendf("Password: %s", gosettings.ObfuscateKey(*s.Password))
76+
}
77+
return node
78+
}
79+
80+
func (s *Socks5) read(r *reader.Reader) (err error) {
81+
s.Enabled, err = r.BoolPtr("SOCKS5_ENABLED")
82+
if err != nil {
83+
return err
84+
}
85+
86+
s.ListeningAddress = r.String("SOCKS5_LISTENING_ADDRESS")
87+
s.Username = r.Get("SOCKS5_USER", reader.ForceLowercase(false))
88+
s.Password = r.Get("SOCKS5_PASSWORD", reader.ForceLowercase(false))
89+
90+
return nil
91+
}

internal/socks5/constants.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package socks5
2+
3+
import "fmt"
4+
5+
// See https://datatracker.ietf.org/doc/html/rfc1928#section-3
6+
type authMethod byte
7+
8+
const (
9+
authNotRequired authMethod = 0
10+
authGssapi authMethod = 1
11+
authUsernamePassword authMethod = 2
12+
authNotAcceptable authMethod = 255
13+
)
14+
15+
func (a authMethod) String() string {
16+
switch a {
17+
case authNotRequired:
18+
return "no authentication required"
19+
case authGssapi:
20+
return "GSSAPI"
21+
case authUsernamePassword:
22+
return "username/password"
23+
case authNotAcceptable:
24+
return "no acceptable methods"
25+
default:
26+
return fmt.Sprintf("unknown method (%d)", a)
27+
}
28+
}
29+
30+
// Subnegotiation version
31+
// See https://datatracker.ietf.org/doc/html/rfc1929#section-2
32+
const (
33+
authUsernamePasswordSubNegotiation1 byte = 1
34+
)
35+
36+
// SOCKS versions.
37+
const (
38+
socks5Version byte = 5
39+
)
40+
41+
// See https://datatracker.ietf.org/doc/html/rfc1928#section-4
42+
type cmdType byte
43+
44+
const (
45+
connect cmdType = 1
46+
bind cmdType = 2
47+
udpAssociate cmdType = 3
48+
)
49+
50+
func (c cmdType) String() string {
51+
switch c {
52+
case connect:
53+
return "connect"
54+
case bind:
55+
return "bind"
56+
case udpAssociate:
57+
return "UDP associate"
58+
default:
59+
return fmt.Sprintf("unknown command (%d)", c)
60+
}
61+
}
62+
63+
// See https://datatracker.ietf.org/doc/html/rfc1928#section-4 and
64+
// https://datatracker.ietf.org/doc/html/rfc1928#section-5
65+
type addrType byte
66+
67+
const (
68+
ipv4 addrType = 1
69+
domainName addrType = 3
70+
ipv6 addrType = 4
71+
)
72+
73+
// See https://datatracker.ietf.org/doc/html/rfc1928#section-6
74+
type replyCode byte
75+
76+
const (
77+
succeeded replyCode = iota
78+
generalServerFailure
79+
connectionNotAllowedByRuleset
80+
networkUnreachable
81+
hostUnreachable
82+
connectionRefused
83+
ttlExpired
84+
commandNotSupported
85+
addressTypeNotSupported
86+
)

internal/socks5/interfaces.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package socks5
2+
3+
type Logger interface {
4+
Infof(format string, a ...interface{})
5+
Warnf(format string, a ...interface{})
6+
}

0 commit comments

Comments
 (0)