Shadowsocks in Go (#220), fixes #211

This commit is contained in:
Quentin McGaw
2020-08-20 19:19:54 -04:00
committed by GitHub
parent b10a476622
commit c614a192a4
11 changed files with 53 additions and 238 deletions

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 (
"context"
"fmt"
"sync"
"time"
"github.com/qdm12/gluetun/internal/firewall"
"github.com/qdm12/gluetun/internal/settings"
"github.com/qdm12/golibs/command"
"github.com/qdm12/golibs/logging"
shadowsockslib "github.com/qdm12/ss-server/pkg"
)
type Looper interface {
@@ -21,15 +22,10 @@ type Looper interface {
}
type looper struct {
conf Configurator
firewallConf firewall.Configurator
settings settings.ShadowSocks
settingsMutex sync.RWMutex
dnsSettings settings.DNS // TODO
logger logging.Logger
streamMerger command.StreamMerger
uid int
gid int
defaultInterface string
restart chan struct{}
start chan struct{}
@@ -44,17 +40,12 @@ func (l *looper) logAndWait(ctx context.Context, err error) {
<-ctx.Done()
}
func NewLooper(conf Configurator, firewallConf firewall.Configurator, settings settings.ShadowSocks, dnsSettings settings.DNS,
logger logging.Logger, streamMerger command.StreamMerger, uid, gid int, defaultInterface string) Looper {
func NewLooper(firewallConf firewall.Configurator, settings settings.ShadowSocks,
logger logging.Logger, defaultInterface string) Looper {
return &looper{
conf: conf,
firewallConf: firewallConf,
settings: settings,
dnsSettings: dnsSettings,
logger: logger.WithPrefix("shadowsocks: "),
streamMerger: streamMerger,
uid: uid,
gid: gid,
defaultInterface: defaultInterface,
restart: 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()
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 {
l.logAndWait(ctx, err)
continue
@@ -150,19 +137,16 @@ func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup) {
previousPort = settings.Port
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 {
shadowsocksCancel()
l.logAndWait(ctx, err)
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
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()}
}