wire up everything with gluetun

This commit is contained in:
Quentin McGaw
2026-05-19 12:47:35 +00:00
parent f35dd19ee7
commit 7ef16a5438
8 changed files with 201 additions and 5 deletions
+5
View File
@@ -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" \
+15 -1
View File
@@ -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())
@@ -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,
@@ -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:
+83
View File
@@ -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
}
+85
View File
@@ -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():
}
}
+1 -1
View File
@@ -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,
+3 -3
View File
@@ -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,
})