Shadowsocks in Go, refers to #211

This commit is contained in:
Quentin McGaw
2020-08-16 23:07:15 +00:00
parent c59447c646
commit 02930b6a95
9 changed files with 52 additions and 235 deletions

View File

@@ -69,7 +69,6 @@ func _main(background context.Context, args []string) int {
routingConf := routing.NewRouting(logger, fileManager) routingConf := routing.NewRouting(logger, fileManager)
firewallConf := firewall.NewConfigurator(logger, routingConf, fileManager) firewallConf := firewall.NewConfigurator(logger, routingConf, fileManager)
tinyProxyConf := tinyproxy.NewConfigurator(fileManager, logger) tinyProxyConf := tinyproxy.NewConfigurator(fileManager, logger)
shadowsocksConf := shadowsocks.NewConfigurator(fileManager, logger)
streamMerger := command.NewStreamMerger() streamMerger := command.NewStreamMerger()
paramsReader := params.NewReader(logger, fileManager) paramsReader := params.NewReader(logger, fileManager)
@@ -79,11 +78,10 @@ func _main(background context.Context, args []string) int {
paramsReader.GetBuildDate())) paramsReader.GetBuildDate()))
printVersions(ctx, logger, map[string]func(ctx context.Context) (string, error){ printVersions(ctx, logger, map[string]func(ctx context.Context) (string, error){
"OpenVPN": ovpnConf.Version, "OpenVPN": ovpnConf.Version,
"Unbound": dnsConf.Version, "Unbound": dnsConf.Version,
"IPtables": firewallConf.Version, "IPtables": firewallConf.Version,
"TinyProxy": tinyProxyConf.Version, "TinyProxy": tinyProxyConf.Version,
"ShadowSocks": shadowsocksConf.Version,
}) })
allSettings, err := settings.GetAllSettings(paramsReader) allSettings, err := settings.GetAllSettings(paramsReader)
@@ -170,7 +168,7 @@ func _main(background context.Context, args []string) int {
restartTinyproxy := tinyproxyLooper.Restart restartTinyproxy := tinyproxyLooper.Restart
go tinyproxyLooper.Run(ctx, wg) go tinyproxyLooper.Run(ctx, wg)
shadowsocksLooper := shadowsocks.NewLooper(shadowsocksConf, firewallConf, allSettings.ShadowSocks, allSettings.DNS, logger, streamMerger, uid, gid, defaultInterface) shadowsocksLooper := shadowsocks.NewLooper(firewallConf, allSettings.ShadowSocks, logger, defaultInterface)
restartShadowsocks := shadowsocksLooper.Restart restartShadowsocks := shadowsocksLooper.Restart
go shadowsocksLooper.Run(ctx, wg) go shadowsocksLooper.Run(ctx, wg)

1
go.mod
View File

@@ -7,6 +7,7 @@ require (
github.com/golang/mock v1.4.4 github.com/golang/mock v1.4.4
github.com/kyokomi/emoji v2.2.4+incompatible github.com/kyokomi/emoji v2.2.4+incompatible
github.com/qdm12/golibs v0.0.0-20200712151944-a0325873bf5a github.com/qdm12/golibs v0.0.0-20200712151944-a0325873bf5a
github.com/qdm12/ss-server v0.0.0-20200816230146-a9af69e3f62b
github.com/stretchr/testify v1.6.1 github.com/stretchr/testify v1.6.1
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed
) )

4
go.sum
View File

@@ -74,6 +74,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/qdm12/golibs v0.0.0-20200712151944-a0325873bf5a h1:IyS72qFm+iXipadmUKXmpJScKXXK2GrD8yYfxXsnIYs= github.com/qdm12/golibs v0.0.0-20200712151944-a0325873bf5a h1:IyS72qFm+iXipadmUKXmpJScKXXK2GrD8yYfxXsnIYs=
github.com/qdm12/golibs v0.0.0-20200712151944-a0325873bf5a/go.mod h1:pikkTN7g7zRuuAnERwqW1yAFq6pYmxrxpjiwGvb0Ysc= github.com/qdm12/golibs v0.0.0-20200712151944-a0325873bf5a/go.mod h1:pikkTN7g7zRuuAnERwqW1yAFq6pYmxrxpjiwGvb0Ysc=
github.com/qdm12/ss-server v0.0.0-20200816230146-a9af69e3f62b h1:cNsfEvt43ZTYCGDiZWfWrl9l/a7WWy3QWsRtRwkhnwg=
github.com/qdm12/ss-server v0.0.0-20200816230146-a9af69e3f62b/go.mod h1:ABVUkxubboL3vqBkOwDV9glX1/x7SnYrckBe5d+M/zw=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=

View File

@@ -1,41 +0,0 @@
package shadowsocks
import (
"context"
"fmt"
"io"
"strings"
"github.com/qdm12/gluetun/internal/constants"
)
func (c *configurator) Start(ctx context.Context, server string, port uint16, password string, log bool) (stdout, stderr io.ReadCloser, waitFn func() error, err error) {
c.logger.Info("starting shadowsocks server")
args := []string{
"-c", string(constants.ShadowsocksConf),
"-p", fmt.Sprintf("%d", port),
"-k", password,
}
if log {
args = append(args, "-v")
}
stdout, stderr, waitFn, err = c.commander.Start(ctx, "ss-server", args...)
return stdout, stderr, waitFn, err
}
// Version obtains the version of the installed shadowsocks server
func (c *configurator) Version(ctx context.Context) (string, error) {
output, err := c.commander.Run(ctx, "ss-server", "-h")
if err != nil {
return "", err
}
lines := strings.Split(output, "\n")
if len(lines) < 2 {
return "", fmt.Errorf("ss-server -h: not enough lines in %q", output)
}
words := strings.Fields(lines[1])
if len(words) < 2 {
return "", fmt.Errorf("ss-server -h: line 2 is too short: %q", lines[1])
}
return words[1], nil
}

View File

@@ -1,51 +0,0 @@
package shadowsocks
import (
"encoding/json"
"fmt"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/golibs/files"
)
func (c *configurator) MakeConf(port uint16, password, method, nameserver string, uid, gid int) (err error) {
c.logger.Info("generating configuration file")
data := generateConf(port, password, method, nameserver)
return c.fileManager.WriteToFile(
string(constants.ShadowsocksConf),
data,
files.Ownership(uid, gid),
files.Permissions(0400))
}
func generateConf(port uint16, password, method, nameserver string) (data []byte) {
conf := struct {
Server string `json:"server"`
User string `json:"user"`
Method string `json:"method"`
Timeout uint `json:"timeout"`
FastOpen bool `json:"fast_open"`
Mode string `json:"mode"`
PortPassword map[string]string `json:"port_password"`
Workers uint `json:"workers"`
Interface string `json:"interface"`
Nameserver *string `json:"nameserver,omitempty"`
}{
Server: "0.0.0.0",
User: "nonrootuser",
Method: method,
Timeout: 30,
FastOpen: false,
Mode: "tcp_and_udp",
PortPassword: map[string]string{
fmt.Sprintf("%d", port): password,
},
Workers: 2,
Interface: "tun",
}
if len(nameserver) > 0 {
conf.Nameserver = &nameserver
}
data, _ = json.Marshal(conf)
return data
}

View File

@@ -1,81 +0,0 @@
package shadowsocks
import (
"fmt"
"testing"
"github.com/golang/mock/gomock"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/golibs/files"
"github.com/qdm12/golibs/files/mock_files"
"github.com/qdm12/golibs/logging/mock_logging"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_generateConf(t *testing.T) {
t.Parallel()
tests := map[string]struct {
port uint16
password string
nameserver string
data []byte
}{
"no data": {
data: []byte(`{"server":"0.0.0.0","user":"nonrootuser","method":"chacha20-ietf-poly1305","timeout":30,"fast_open":false,"mode":"tcp_and_udp","port_password":{"0":""},"workers":2,"interface":"tun"}`),
},
"data": {
port: 2000,
password: "abcde",
nameserver: "127.0.0.1",
data: []byte(`{"server":"0.0.0.0","user":"nonrootuser","method":"chacha20-ietf-poly1305","timeout":30,"fast_open":false,"mode":"tcp_and_udp","port_password":{"2000":"abcde"},"workers":2,"interface":"tun","nameserver":"127.0.0.1"}`),
},
}
for name, tc := range tests {
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
data := generateConf(tc.port, tc.password, "chacha20-ietf-poly1305", tc.nameserver)
assert.Equal(t, tc.data, data)
})
}
}
func Test_MakeConf(t *testing.T) {
t.Parallel()
tests := map[string]struct {
writeErr error
err error
}{
"no write error": {},
"write error": {
writeErr: fmt.Errorf("error"),
err: fmt.Errorf("error"),
},
}
for name, tc := range tests {
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
logger := mock_logging.NewMockLogger(mockCtrl)
logger.EXPECT().Info("generating configuration file").Times(1)
fileManager := mock_files.NewMockFileManager(mockCtrl)
fileManager.EXPECT().WriteToFile(
string(constants.ShadowsocksConf),
[]byte(`{"server":"0.0.0.0","user":"nonrootuser","method":"chacha20-ietf-poly1305","timeout":30,"fast_open":false,"mode":"tcp_and_udp","port_password":{"2000":"abcde"},"workers":2,"interface":"tun","nameserver":"127.0.0.1"}`),
gomock.AssignableToTypeOf(files.Ownership(0, 0)),
gomock.AssignableToTypeOf(files.Ownership(0, 0)),
).Return(tc.writeErr).Times(1)
c := &configurator{logger: logger, fileManager: fileManager}
err := c.MakeConf(2000, "abcde", "chacha20-ietf-poly1305", "127.0.0.1", 1000, 1001)
if tc.err != nil {
require.Error(t, err)
assert.Equal(t, tc.err.Error(), err.Error())
} else {
assert.NoError(t, err)
}
})
}
}

View File

@@ -0,0 +1,32 @@
package shadowsocks
import "github.com/qdm12/golibs/logging"
type logAdapter struct {
logger logging.Logger
enabled bool
}
func (l *logAdapter) Info(s string) {
if l.enabled {
l.logger.Info(s)
}
}
func (l *logAdapter) Debug(s string) {
if l.enabled {
l.logger.Debug(s)
}
}
func (l *logAdapter) Error(s string) {
if l.enabled {
l.logger.Error(s)
}
}
func adaptLogger(logger logging.Logger, enabled bool) *logAdapter {
return &logAdapter{
logger: logger,
enabled: enabled,
}
}

View File

@@ -2,13 +2,14 @@ package shadowsocks
import ( import (
"context" "context"
"fmt"
"sync" "sync"
"time" "time"
"github.com/qdm12/gluetun/internal/firewall" "github.com/qdm12/gluetun/internal/firewall"
"github.com/qdm12/gluetun/internal/settings" "github.com/qdm12/gluetun/internal/settings"
"github.com/qdm12/golibs/command"
"github.com/qdm12/golibs/logging" "github.com/qdm12/golibs/logging"
shadowsockslib "github.com/qdm12/ss-server/pkg"
) )
type Looper interface { type Looper interface {
@@ -21,15 +22,10 @@ type Looper interface {
} }
type looper struct { type looper struct {
conf Configurator
firewallConf firewall.Configurator firewallConf firewall.Configurator
settings settings.ShadowSocks settings settings.ShadowSocks
settingsMutex sync.RWMutex settingsMutex sync.RWMutex
dnsSettings settings.DNS // TODO
logger logging.Logger logger logging.Logger
streamMerger command.StreamMerger
uid int
gid int
defaultInterface string defaultInterface string
restart chan struct{} restart chan struct{}
start chan struct{} start chan struct{}
@@ -44,17 +40,12 @@ func (l *looper) logAndWait(ctx context.Context, err error) {
<-ctx.Done() <-ctx.Done()
} }
func NewLooper(conf Configurator, firewallConf firewall.Configurator, settings settings.ShadowSocks, dnsSettings settings.DNS, func NewLooper(firewallConf firewall.Configurator, settings settings.ShadowSocks,
logger logging.Logger, streamMerger command.StreamMerger, uid, gid int, defaultInterface string) Looper { logger logging.Logger, defaultInterface string) Looper {
return &looper{ return &looper{
conf: conf,
firewallConf: firewallConf, firewallConf: firewallConf,
settings: settings, settings: settings,
dnsSettings: dnsSettings,
logger: logger.WithPrefix("shadowsocks: "), logger: logger.WithPrefix("shadowsocks: "),
streamMerger: streamMerger,
uid: uid,
gid: gid,
defaultInterface: defaultInterface, defaultInterface: defaultInterface,
restart: make(chan struct{}), restart: make(chan struct{}),
start: make(chan struct{}), start: make(chan struct{}),
@@ -126,12 +117,8 @@ func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup) {
} }
} }
nameserver := l.dnsSettings.PlaintextAddress.String()
if l.dnsSettings.Enabled {
nameserver = "127.0.0.1"
}
settings := l.GetSettings() settings := l.GetSettings()
err := l.conf.MakeConf(settings.Port, settings.Password, settings.Method, nameserver, l.uid, l.gid) server, err := shadowsockslib.NewServer(settings.Method, settings.Password, adaptLogger(l.logger, settings.Log))
if err != nil { if err != nil {
l.logAndWait(ctx, err) l.logAndWait(ctx, err)
continue continue
@@ -150,19 +137,16 @@ func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup) {
previousPort = settings.Port previousPort = settings.Port
shadowsocksCtx, shadowsocksCancel := context.WithCancel(context.Background()) shadowsocksCtx, shadowsocksCancel := context.WithCancel(context.Background())
stdout, stderr, waitFn, err := l.conf.Start(shadowsocksCtx, "0.0.0.0", settings.Port, settings.Password, settings.Log)
waitError := make(chan error)
go func() {
waitError <- server.Listen(shadowsocksCtx, fmt.Sprintf("0.0.0.0:%d", settings.Port))
}()
if err != nil { if err != nil {
shadowsocksCancel() shadowsocksCancel()
l.logAndWait(ctx, err) l.logAndWait(ctx, err)
continue continue
} }
go l.streamMerger.Merge(shadowsocksCtx, stdout, command.MergeName("shadowsocks"))
go l.streamMerger.Merge(shadowsocksCtx, stderr, command.MergeName("shadowsocks error"))
waitError := make(chan error)
go func() {
err := waitFn() // blocking
waitError <- err
}()
stayHere := true stayHere := true
for stayHere { for stayHere {

View File

@@ -1,29 +0,0 @@
package shadowsocks
import (
"context"
"io"
"github.com/qdm12/golibs/command"
"github.com/qdm12/golibs/files"
"github.com/qdm12/golibs/logging"
)
type Configurator interface {
Version(ctx context.Context) (string, error)
MakeConf(port uint16, password, method, nameserver string, uid, gid int) (err error)
Start(ctx context.Context, server string, port uint16, password string, log bool) (stdout, stderr io.ReadCloser, waitFn func() error, err error)
}
type configurator struct {
fileManager files.FileManager
logger logging.Logger
commander command.Commander
}
func NewConfigurator(fileManager files.FileManager, logger logging.Logger) Configurator {
return &configurator{
fileManager: fileManager,
logger: logger.WithPrefix("shadowsocks configurator: "),
commander: command.NewCommander()}
}