Maintenance: improve stream merging

This commit is contained in:
Quentin McGaw
2021-01-26 04:17:22 +00:00
parent 937d09f1c3
commit a243d48fb1
13 changed files with 321 additions and 248 deletions

View File

@@ -7,7 +7,6 @@ import (
"net/http" "net/http"
nativeos "os" nativeos "os"
"os/signal" "os/signal"
"strings"
"sync" "sync"
"syscall" "syscall"
"time" "time"
@@ -33,7 +32,6 @@ import (
"github.com/qdm12/gluetun/internal/unix" "github.com/qdm12/gluetun/internal/unix"
"github.com/qdm12/gluetun/internal/updater" "github.com/qdm12/gluetun/internal/updater"
versionpkg "github.com/qdm12/gluetun/internal/version" versionpkg "github.com/qdm12/gluetun/internal/version"
"github.com/qdm12/golibs/command"
"github.com/qdm12/golibs/logging" "github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os" "github.com/qdm12/golibs/os"
"github.com/qdm12/golibs/os/user" "github.com/qdm12/golibs/os/user"
@@ -141,7 +139,6 @@ func _main(background context.Context, buildInfo models.BuildInformation,
"/etc/unbound", "/usr/sbin/unbound", cacertsPath) "/etc/unbound", "/usr/sbin/unbound", cacertsPath)
routingConf := routing.NewRouting(logger) routingConf := routing.NewRouting(logger)
firewallConf := firewall.NewConfigurator(logger, routingConf, os.OpenFile) firewallConf := firewall.NewConfigurator(logger, routingConf, os.OpenFile)
streamMerger := command.NewStreamMerger()
paramsReader := params.NewReader(logger, os) paramsReader := params.NewReader(logger, os)
fmt.Println(gluetunLogging.Splash(buildInfo)) fmt.Println(gluetunLogging.Splash(buildInfo))
@@ -266,11 +263,8 @@ func _main(background context.Context, buildInfo models.BuildInformation,
wg := &sync.WaitGroup{} wg := &sync.WaitGroup{}
wg.Add(1)
go collectStreamLines(ctx, wg, streamMerger, logger, tunnelReadyCh)
openvpnLooper := openvpn.NewLooper(allSettings.OpenVPN, nonRootUsername, puid, pgid, allServers, openvpnLooper := openvpn.NewLooper(allSettings.OpenVPN, nonRootUsername, puid, pgid, allServers,
ovpnConf, firewallConf, routingConf, logger, httpClient, os.OpenFile, streamMerger, cancel) ovpnConf, firewallConf, routingConf, logger, httpClient, os.OpenFile, tunnelReadyCh, cancel)
wg.Add(1) wg.Add(1)
// wait for restartOpenvpn // wait for restartOpenvpn
go openvpnLooper.Run(ctx, wg) go openvpnLooper.Run(ctx, wg)
@@ -282,7 +276,7 @@ func _main(background context.Context, buildInfo models.BuildInformation,
go updaterLooper.Run(ctx, wg) go updaterLooper.Run(ctx, wg)
unboundLooper := dns.NewLooper(dnsConf, allSettings.DNS, httpClient, unboundLooper := dns.NewLooper(dnsConf, allSettings.DNS, httpClient,
logger, streamMerger, nonRootUsername, puid, pgid) logger, nonRootUsername, puid, pgid)
wg.Add(1) wg.Add(1)
// wait for unboundLooper.Restart or its ticker launched with RunRestartTicker // wait for unboundLooper.Restart or its ticker launched with RunRestartTicker
go unboundLooper.Run(ctx, wg, dnsReadyCh) go unboundLooper.Run(ctx, wg, dnsReadyCh)
@@ -352,42 +346,6 @@ func printVersions(ctx context.Context, logger logging.Logger,
} }
} }
func collectStreamLines(ctx context.Context, wg *sync.WaitGroup,
streamMerger command.StreamMerger,
logger logging.Logger, tunnelReadyCh chan<- struct{}) {
defer wg.Done()
// Blocking line merging paramsReader for openvpn and unbound
logger.Info("Launching standard output merger")
streamMerger.CollectLines(ctx, func(line string) {
line, level := gluetunLogging.PostProcessLine(line)
if line == "" {
return
}
switch level {
case logging.DebugLevel:
logger.Debug(line)
case logging.InfoLevel:
logger.Info(line)
case logging.WarnLevel:
logger.Warn(line)
case logging.ErrorLevel:
logger.Error(line)
}
switch {
case strings.Contains(line, "Initialization Sequence Completed"):
tunnelReadyCh <- struct{}{}
case strings.Contains(line, "TLS Error: TLS key negotiation failed to occur within 60 seconds (check your network connectivity)"): //nolint:lll
logger.Warn("This means that either...")
logger.Warn("1. The VPN server IP address you are trying to connect to is no longer valid, see https://github.com/qdm12/gluetun/wiki/Update-servers-information") //nolint:lll
logger.Warn("2. The VPN server crashed, try changing region")
logger.Warn("3. Your Internet connection is not working, ensure it works")
logger.Warn("Feel free to create an issue at https://github.com/qdm12/gluetun/issues/new/choose")
}
}, func(err error) {
logger.Warn(err)
})
}
func routeReadyEvents(ctx context.Context, wg *sync.WaitGroup, buildInfo models.BuildInformation, func routeReadyEvents(ctx context.Context, wg *sync.WaitGroup, buildInfo models.BuildInformation,
tunnelReadyCh, dnsReadyCh <-chan struct{}, tunnelReadyCh, dnsReadyCh <-chan struct{},
unboundLooper dns.Looper, updaterLooper updater.Looper, publicIPLooper publicip.Looper, unboundLooper dns.Looper, updaterLooper updater.Looper, publicIPLooper publicip.Looper,

4
go.mod
View File

@@ -6,8 +6,8 @@ require (
github.com/fatih/color v1.10.0 github.com/fatih/color v1.10.0
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/dns v1.4.0-rc4 github.com/qdm12/dns v1.4.0-rc5
github.com/qdm12/golibs v0.0.0-20210110211000-0a3a4541ae09 github.com/qdm12/golibs v0.0.0-20210124192933-79a950eaf217
github.com/qdm12/ss-server v0.1.0 github.com/qdm12/ss-server v0.1.0
github.com/qdm12/updated v0.0.0-20210102005021-dd457d77f94a github.com/qdm12/updated v0.0.0-20210102005021-dd457d77f94a
github.com/stretchr/testify v1.7.0 github.com/stretchr/testify v1.7.0

10
go.sum
View File

@@ -92,13 +92,11 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
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/dns v1.4.0-rc4 h1:pCcFMqismbktPQX7yrtdmJZq30Y05JBfWRTXY1ZLVFw= github.com/qdm12/dns v1.4.0-rc5 h1:XXjYaFI3pDY1U4YFH5t5AI5IEKlIALmnE34VFhgkdQE=
github.com/qdm12/dns v1.4.0-rc4/go.mod h1:JhUKBhuDRYBUQ2XwW/jbeWx/qS0sSJjIFjGTCFGP5I8= github.com/qdm12/dns v1.4.0-rc5/go.mod h1:WUY4/U8Z2O8888DPrahrIBv8GdYeoIcEy4aUDecZ+UM=
github.com/qdm12/golibs v0.0.0-20201227203847-2fd99ffdfdba/go.mod h1:pikkTN7g7zRuuAnERwqW1yAFq6pYmxrxpjiwGvb0Ysc= github.com/qdm12/golibs v0.0.0-20201227203847-2fd99ffdfdba/go.mod h1:pikkTN7g7zRuuAnERwqW1yAFq6pYmxrxpjiwGvb0Ysc=
github.com/qdm12/golibs v0.0.0-20210102020307-17bc97def973 h1:5YeJALmDjvg2wSi6XB8MpQQekbT/eBnwGahJrh01HHQ= github.com/qdm12/golibs v0.0.0-20210124192933-79a950eaf217 h1:/eMBq0vbc/KmVPXbwLfssp547pp6APRS1x/JNmPvm0s=
github.com/qdm12/golibs v0.0.0-20210102020307-17bc97def973/go.mod h1:pikkTN7g7zRuuAnERwqW1yAFq6pYmxrxpjiwGvb0Ysc= github.com/qdm12/golibs v0.0.0-20210124192933-79a950eaf217/go.mod h1:pikkTN7g7zRuuAnERwqW1yAFq6pYmxrxpjiwGvb0Ysc=
github.com/qdm12/golibs v0.0.0-20210110211000-0a3a4541ae09 h1:zP+ZRwV3GldgTWFgKNBQ2zoFA8mIczb+fvTvrX8LZRo=
github.com/qdm12/golibs v0.0.0-20210110211000-0a3a4541ae09/go.mod h1:pikkTN7g7zRuuAnERwqW1yAFq6pYmxrxpjiwGvb0Ysc=
github.com/qdm12/ss-server v0.1.0 h1:WV9MkHCDEWRwe4WpnYFeR/zcZAxYoTbfntLDnw9AQ50= github.com/qdm12/ss-server v0.1.0 h1:WV9MkHCDEWRwe4WpnYFeR/zcZAxYoTbfntLDnw9AQ50=
github.com/qdm12/ss-server v0.1.0/go.mod h1:ABVUkxubboL3vqBkOwDV9glX1/x7SnYrckBe5d+M/zw= github.com/qdm12/ss-server v0.1.0/go.mod h1:ABVUkxubboL3vqBkOwDV9glX1/x7SnYrckBe5d+M/zw=
github.com/qdm12/updated v0.0.0-20210102005021-dd457d77f94a h1:gkyP+gMEeBgMgyRYGrVNcoy6cL1065IvXsyfB6xboIc= github.com/qdm12/updated v0.0.0-20210102005021-dd457d77f94a h1:gkyP+gMEeBgMgyRYGrVNcoy6cL1065IvXsyfB6xboIc=

61
internal/dns/logs.go Normal file
View File

@@ -0,0 +1,61 @@
package dns
import (
"regexp"
"strings"
"sync"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/golibs/logging"
)
func (l *looper) collectLines(wg *sync.WaitGroup, stdout, stderr <-chan string) {
defer wg.Done()
var line string
var ok bool
for {
select {
case line, ok = <-stderr:
case line, ok = <-stdout:
}
if !ok {
return
}
line, level := processLogLine(line)
switch level {
case logging.DebugLevel:
l.logger.Debug(line)
case logging.InfoLevel:
l.logger.Info(line)
case logging.WarnLevel:
l.logger.Warn(line)
case logging.ErrorLevel:
l.logger.Error(line)
}
}
}
var unboundPrefix = regexp.MustCompile(`\[[0-9]{10}\] unbound\[[0-9]+:[0|1]\] `)
func processLogLine(s string) (filtered string, level logging.Level) {
prefix := unboundPrefix.FindString(s)
filtered = s[len(prefix):]
switch {
case strings.HasPrefix(filtered, "notice: "):
filtered = strings.TrimPrefix(filtered, "notice: ")
level = logging.InfoLevel
case strings.HasPrefix(filtered, "info: "):
filtered = strings.TrimPrefix(filtered, "info: ")
level = logging.InfoLevel
case strings.HasPrefix(filtered, "warn: "):
filtered = strings.TrimPrefix(filtered, "warn: ")
level = logging.WarnLevel
case strings.HasPrefix(filtered, "error: "):
filtered = strings.TrimPrefix(filtered, "error: ")
level = logging.ErrorLevel
default:
level = logging.InfoLevel
}
filtered = constants.ColorUnbound().Sprintf(filtered)
return filtered, level
}

49
internal/dns/logs_test.go Normal file
View File

@@ -0,0 +1,49 @@
package dns
import (
"testing"
"github.com/qdm12/golibs/logging"
"github.com/stretchr/testify/assert"
)
func Test_processLogLine(t *testing.T) {
t.Parallel()
tests := map[string]struct {
s string
filtered string
level logging.Level
}{
"empty string": {"", "", logging.InfoLevel},
"random string": {"asdasqdb", "asdasqdb", logging.InfoLevel},
"unbound notice": {
"[1594595249] unbound[75:0] notice: init module 0: validator",
"init module 0: validator",
logging.InfoLevel},
"unbound info": {
"[1594595249] unbound[75:0] info: init module 0: validator",
"init module 0: validator",
logging.InfoLevel},
"unbound warn": {
"[1594595249] unbound[75:0] warn: init module 0: validator",
"init module 0: validator",
logging.WarnLevel},
"unbound error": {
"[1594595249] unbound[75:0] error: init module 0: validator",
"init module 0: validator",
logging.ErrorLevel},
"unbound unknown": {
"[1594595249] unbound[75:0] BLA: init module 0: validator",
"BLA: init module 0: validator",
logging.InfoLevel},
}
for name, tc := range tests {
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
filtered, level := processLogLine(tc.s)
assert.Equal(t, tc.filtered, filtered)
assert.Equal(t, tc.level, level)
})
}
}

View File

@@ -12,7 +12,6 @@ import (
"github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/models"
"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"
) )
@@ -30,7 +29,6 @@ type looper struct {
conf unbound.Configurator conf unbound.Configurator
client *http.Client client *http.Client
logger logging.Logger logger logging.Logger
streamMerger command.StreamMerger
username string username string
puid int puid int
pgid int pgid int
@@ -48,8 +46,7 @@ type looper struct {
const defaultBackoffTime = 10 * time.Second const defaultBackoffTime = 10 * time.Second
func NewLooper(conf unbound.Configurator, settings settings.DNS, client *http.Client, func NewLooper(conf unbound.Configurator, settings settings.DNS, client *http.Client,
logger logging.Logger, streamMerger command.StreamMerger, logger logging.Logger, username string, puid, pgid int) Looper {
username string, puid, pgid int) Looper {
return &looper{ return &looper{
state: state{ state: state{
status: constants.Stopped, status: constants.Stopped,
@@ -61,7 +58,6 @@ func NewLooper(conf unbound.Configurator, settings settings.DNS, client *http.Cl
username: username, username: username,
puid: puid, puid: puid,
pgid: pgid, pgid: pgid,
streamMerger: streamMerger,
start: make(chan struct{}), start: make(chan struct{}),
running: make(chan models.LoopStatus), running: make(chan models.LoopStatus),
stop: make(chan struct{}), stop: make(chan struct{}),
@@ -107,8 +103,10 @@ func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup, dnsReadyCh chan<-
for ctx.Err() == nil { for ctx.Err() == nil {
// Upper scope variables for Unbound only // Upper scope variables for Unbound only
var unboundCancel context.CancelFunc = func() {} // Their values are to be used if DOT=off
waitError := make(chan error) var waitError chan error
var unboundCancel context.CancelFunc
var closeStreams func()
for l.GetSettings().Enabled { for l.GetSettings().Enabled {
if ctx.Err() != nil { if ctx.Err() != nil {
@@ -116,7 +114,7 @@ func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup, dnsReadyCh chan<-
return return
} }
var err error var err error
unboundCancel, err = l.setupUnbound(ctx, crashed, waitError) unboundCancel, waitError, closeStreams, err = l.setupUnbound(ctx, wg, crashed)
if err != nil { if err != nil {
if !errors.Is(err, errUpdateFiles) { if !errors.Is(err, errUpdateFiles) {
const fallback = true const fallback = true
@@ -130,6 +128,9 @@ func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup, dnsReadyCh chan<-
if !l.GetSettings().Enabled { if !l.GetSettings().Enabled {
const fallback = false const fallback = false
l.useUnencryptedDNS(fallback) l.useUnencryptedDNS(fallback)
waitError := make(chan error)
unboundCancel = func() { waitError <- nil }
closeStreams = func() {}
} }
dnsReadyCh <- struct{}{} dnsReadyCh <- struct{}{}
@@ -142,6 +143,7 @@ func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup, dnsReadyCh chan<-
unboundCancel() unboundCancel()
<-waitError <-waitError
close(waitError) close(waitError)
closeStreams()
return return
case <-l.stop: case <-l.stop:
l.logger.Info("stopping") l.logger.Info("stopping")
@@ -163,7 +165,7 @@ func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup, dnsReadyCh chan<-
} }
} }
close(waitError) close(waitError)
unboundCancel() closeStreams()
} }
} }
@@ -172,28 +174,29 @@ var errUpdateFiles = errors.New("cannot update files")
// Returning cancel == nil signals we want to re-run setupUnbound // Returning cancel == nil signals we want to re-run setupUnbound
// Returning err == errUpdateFiles signals we should not fall back // Returning err == errUpdateFiles signals we should not fall back
// on the plaintext DNS as DOT is still up and running. // on the plaintext DNS as DOT is still up and running.
func (l *looper) setupUnbound(ctx context.Context, func (l *looper) setupUnbound(ctx context.Context, wg *sync.WaitGroup,
previousCrashed bool, waitError chan<- error) (cancel context.CancelFunc, err error) { previousCrashed bool) (cancel context.CancelFunc, waitError chan error,
closeStreams func(), err error) {
err = l.updateFiles(ctx) err = l.updateFiles(ctx)
if err != nil { if err != nil {
l.state.setStatusWithLock(constants.Crashed) l.state.setStatusWithLock(constants.Crashed)
return nil, errUpdateFiles return nil, nil, nil, errUpdateFiles
} }
settings := l.GetSettings() settings := l.GetSettings()
unboundCtx, cancel := context.WithCancel(context.Background()) unboundCtx, cancel := context.WithCancel(context.Background())
stream, waitFn, err := l.conf.Start(unboundCtx, settings.Unbound.VerbosityDetailsLevel) stdoutLines, stderrLines, waitError, err := l.conf.Start(unboundCtx, settings.Unbound.VerbosityDetailsLevel)
if err != nil { if err != nil {
cancel() cancel()
if !previousCrashed { if !previousCrashed {
l.running <- constants.Crashed l.running <- constants.Crashed
} }
return nil, err return nil, nil, nil, err
} }
// Started successfully wg.Add(1)
go l.streamMerger.Merge(unboundCtx, stream, command.MergeName("unbound")) go l.collectLines(wg, stdoutLines, stderrLines)
l.conf.UseDNSInternally(net.IP{127, 0, 0, 1}) // use Unbound l.conf.UseDNSInternally(net.IP{127, 0, 0, 1}) // use Unbound
if err := l.conf.UseDNSSystemWide(net.IP{127, 0, 0, 1}, settings.KeepNameserver); err != nil { // use Unbound if err := l.conf.UseDNSSystemWide(net.IP{127, 0, 0, 1}, settings.KeepNameserver); err != nil { // use Unbound
@@ -205,14 +208,13 @@ func (l *looper) setupUnbound(ctx context.Context,
l.running <- constants.Crashed l.running <- constants.Crashed
} }
cancel() cancel()
return nil, err <-waitError
close(waitError)
close(stdoutLines)
close(stderrLines)
return nil, nil, nil, err
} }
go func() {
err := waitFn() // blocking
waitError <- err
}()
l.logger.Info("ready") l.logger.Info("ready")
if !previousCrashed { if !previousCrashed {
l.running <- constants.Running l.running <- constants.Running
@@ -220,7 +222,13 @@ func (l *looper) setupUnbound(ctx context.Context,
l.backoffTime = defaultBackoffTime l.backoffTime = defaultBackoffTime
l.state.setStatusWithLock(constants.Running) l.state.setStatusWithLock(constants.Running)
} }
return cancel, nil
closeStreams = func() {
close(stdoutLines)
close(stderrLines)
}
return cancel, waitError, closeStreams, nil
} }
func (l *looper) useUnencryptedDNS(fallback bool) { func (l *looper) useUnencryptedDNS(fallback bool) {

View File

@@ -1,78 +0,0 @@
package logging
import (
"fmt"
"regexp"
"strings"
"github.com/fatih/color"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/golibs/logging"
)
var regularExpressions = struct { //nolint:gochecknoglobals
unboundPrefix *regexp.Regexp
}{
unboundPrefix: regexp.MustCompile(`unbound: \[[0-9]{10}\] unbound\[[0-9]+:[0|1]\] `),
}
func PostProcessLine(s string) (filtered string, level logging.Level) {
switch {
case strings.HasPrefix(s, "openvpn: "):
for _, ignored := range []string{
"openvpn: WARNING: you are using user/group/chroot/setcon without persist-tun -- this may cause restarts to fail",
"openvpn: NOTE: UID/GID downgrade will be delayed because of --client, --pull, or --up-delay",
} {
if s == ignored {
return "", ""
}
}
switch {
case strings.HasPrefix(s, "openvpn: NOTE: "):
filtered = strings.TrimPrefix(s, "openvpn: NOTE: ")
filtered = "openvpn: " + filtered
level = logging.InfoLevel
case strings.HasPrefix(s, "openvpn: WARNING: "):
filtered = strings.TrimPrefix(s, "openvpn: WARNING: ")
filtered = "openvpn: " + filtered
level = logging.WarnLevel
case strings.HasPrefix(s, "openvpn: Options error: "):
filtered = strings.TrimPrefix(s, "openvpn: Options error: ")
filtered = "openvpn: " + filtered
level = logging.ErrorLevel
case s == "openvpn: Initialization Sequence Completed":
return color.HiGreenString(s), logging.InfoLevel
case s == "openvpn: AUTH: Received control message: AUTH_FAILED":
filtered = s + "\n\n (IF YOU ARE USING PIA servers, MAYBE CHECK OUT https://github.com/qdm12/gluetun/issues/265)\n" //nolint:lll
level = logging.ErrorLevel
default:
filtered = s
level = logging.InfoLevel
}
filtered = constants.ColorOpenvpn().Sprintf(filtered)
return filtered, level
case strings.HasPrefix(s, "unbound: "):
prefix := regularExpressions.unboundPrefix.FindString(s)
filtered = s[len(prefix):]
switch {
case strings.HasPrefix(filtered, "notice: "):
filtered = strings.TrimPrefix(filtered, "notice: ")
level = logging.InfoLevel
case strings.HasPrefix(filtered, "info: "):
filtered = strings.TrimPrefix(filtered, "info: ")
level = logging.InfoLevel
case strings.HasPrefix(filtered, "warn: "):
filtered = strings.TrimPrefix(filtered, "warn: ")
level = logging.WarnLevel
case strings.HasPrefix(filtered, "error: "):
filtered = strings.TrimPrefix(filtered, "error: ")
level = logging.ErrorLevel
default:
level = logging.ErrorLevel
}
filtered = fmt.Sprintf("unbound: %s", filtered)
filtered = constants.ColorUnbound().Sprintf(filtered)
return filtered, level
}
return s, logging.InfoLevel
}

View File

@@ -1,77 +0,0 @@
package logging
import (
"testing"
"github.com/qdm12/golibs/logging"
"github.com/stretchr/testify/assert"
)
func Test_PostProcessLine(t *testing.T) {
t.Parallel()
tests := map[string]struct {
s string
filtered string
level logging.Level
}{
"empty string": {"", "", logging.InfoLevel},
"random string": {"asdasqdb", "asdasqdb", logging.InfoLevel},
"unbound notice": {
"unbound: [1594595249] unbound[75:0] notice: init module 0: validator",
"unbound: init module 0: validator",
logging.InfoLevel},
"unbound info": {
"unbound: [1594595249] unbound[75:0] info: init module 0: validator",
"unbound: init module 0: validator",
logging.InfoLevel},
"unbound warn": {
"unbound: [1594595249] unbound[75:0] warn: init module 0: validator",
"unbound: init module 0: validator",
logging.WarnLevel},
"unbound error": {
"unbound: [1594595249] unbound[75:0] error: init module 0: validator",
"unbound: init module 0: validator",
logging.ErrorLevel},
"unbound unknown": {
"unbound: [1594595249] unbound[75:0] BLA: init module 0: validator",
"unbound: BLA: init module 0: validator",
logging.ErrorLevel},
"openvpn unknown": {
"openvpn: message",
"openvpn: message",
logging.InfoLevel},
"openvpn note": {
"openvpn: NOTE: message",
"openvpn: message",
logging.InfoLevel},
"openvpn warning": {
"openvpn: WARNING: message",
"openvpn: message",
logging.WarnLevel},
"openvpn options error": {
"openvpn: Options error: message",
"openvpn: message",
logging.ErrorLevel},
"openvpn ignored message": {
"openvpn: NOTE: UID/GID downgrade will be delayed because of --client, --pull, or --up-delay",
"",
""},
"openvpn success": {
"openvpn: Initialization Sequence Completed",
"openvpn: Initialization Sequence Completed",
logging.InfoLevel},
"openvpn auth failed": {
"openvpn: AUTH: Received control message: AUTH_FAILED",
"openvpn: AUTH: Received control message: AUTH_FAILED\n\n (IF YOU ARE USING PIA servers, MAYBE CHECK OUT https://github.com/qdm12/gluetun/issues/265)\n", //nolint:lll
logging.ErrorLevel},
}
for name, tc := range tests {
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
filtered, level := PostProcessLine(tc.s)
assert.Equal(t, tc.filtered, filtered)
assert.Equal(t, tc.level, level)
})
}
}

View File

@@ -3,16 +3,14 @@ package openvpn
import ( import (
"context" "context"
"fmt" "fmt"
"io"
"strings" "strings"
"github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/constants"
) )
func (c *configurator) Start(ctx context.Context) (stdout io.ReadCloser, waitFn func() error, err error) { func (c *configurator) Start(ctx context.Context) (stdoutLines, stderrLines chan string, waitError chan error, err error) {
c.logger.Info("starting openvpn") c.logger.Info("starting openvpn")
stdout, _, waitFn, err = c.commander.Start(ctx, "openvpn", "--config", string(constants.OpenVPNConf)) return c.commander.Start(ctx, "openvpn", "--config", string(constants.OpenVPNConf))
return stdout, waitFn, err
} }
func (c *configurator) Version(ctx context.Context) (string, error) { func (c *configurator) Version(ctx context.Context) (string, error) {

100
internal/openvpn/logs.go Normal file
View File

@@ -0,0 +1,100 @@
package openvpn
import (
"strings"
"sync"
"github.com/fatih/color"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/golibs/logging"
)
func (l *looper) collectLines(wg *sync.WaitGroup, stdout, stderr <-chan string) {
defer wg.Done()
var line string
var ok, errLine bool
for {
errLine = false
select {
case line, ok = <-stdout:
case line, ok = <-stderr:
errLine = true
}
if !ok {
return
}
line, level := processLogLine(line)
if len(line) == 0 {
continue // filtered out
}
if errLine {
level = logging.ErrorLevel
}
switch level {
case logging.DebugLevel:
l.logger.Debug(line)
case logging.InfoLevel:
l.logger.Info(line)
case logging.WarnLevel:
l.logger.Warn(line)
case logging.ErrorLevel:
l.logger.Error(line)
}
if strings.Contains(line, "Initialization Sequence Completed") {
l.tunnelReady <- struct{}{}
}
}
}
func processLogLine(s string) (filtered string, level logging.Level) {
for _, ignored := range []string{
"WARNING: you are using user/group/chroot/setcon without persist-tun -- this may cause restarts to fail",
"NOTE: UID/GID downgrade will be delayed because of --client, --pull, or --up-delay",
} {
if s == ignored {
return "", ""
}
}
switch {
case strings.HasPrefix(s, "NOTE: "):
filtered = strings.TrimPrefix(s, "NOTE: ")
level = logging.InfoLevel
case strings.HasPrefix(s, "WARNING: "):
filtered = strings.TrimPrefix(s, "WARNING: ")
level = logging.WarnLevel
case strings.HasPrefix(s, "Options error: "):
filtered = strings.TrimPrefix(s, "Options error: ")
level = logging.ErrorLevel
case s == "Initialization Sequence Completed":
return color.HiGreenString(s), logging.InfoLevel
case s == "AUTH: Received control message: AUTH_FAILED":
filtered = s + `
Your credentials might be wrong 🤨
💡 If you use Private Internet Access, check https://github.com/qdm12/gluetun/issues/265
`
level = logging.ErrorLevel
case strings.Contains(s, "TLS Error: TLS key negotiation failed to occur within 60 seconds (check your network connectivity)"):
s += `
🚒🚒🚒🚒🚒🚨🚨🚨🚨🚨🚨🚒🚒🚒🚒🚒
That error usually happens because either:
1. The VPN server IP address you are trying to connect to is no longer valid 🔌
Update your server information using https://github.com/qdm12/gluetun/wiki/Updating-Servers
2. The VPN server crashed 💥, try changing your VPN servers filtering options such as REGION
3. Your Internet connection is not working 🤯, ensure it works
4. Something else ➡️ https://github.com/qdm12/gluetun/issues/new/choose
`
default:
filtered = s
level = logging.InfoLevel
}
filtered = constants.ColorOpenvpn().Sprintf(filtered)
return filtered, level
}

View File

@@ -0,0 +1,57 @@
package openvpn
import (
"testing"
"github.com/qdm12/golibs/logging"
"github.com/stretchr/testify/assert"
)
func Test_processLogLine(t *testing.T) {
t.Parallel()
tests := map[string]struct {
s string
filtered string
level logging.Level
}{
"empty string": {"", "", logging.InfoLevel},
"random string": {"asdasqdb", "asdasqdb", logging.InfoLevel},
"openvpn unknown": {
"message",
"message",
logging.InfoLevel},
"openvpn note": {
"NOTE: message",
"message",
logging.InfoLevel},
"openvpn warning": {
"WARNING: message",
"message",
logging.WarnLevel},
"openvpn options error": {
"Options error: message",
"message",
logging.ErrorLevel},
"openvpn ignored message": {
"NOTE: UID/GID downgrade will be delayed because of --client, --pull, or --up-delay",
"",
""},
"openvpn success": {
"Initialization Sequence Completed",
"Initialization Sequence Completed",
logging.InfoLevel},
"openvpn auth failed": {
"AUTH: Received control message: AUTH_FAILED",
"AUTH: Received control message: AUTH_FAILED\n\nYour credentials might be wrong 🤨\n\n💡 If you use Private Internet Access, check https://github.com/qdm12/gluetun/issues/265\n\n", //nolint:lll
logging.ErrorLevel},
}
for name, tc := range tests {
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
filtered, level := processLogLine(tc.s)
assert.Equal(t, tc.filtered, filtered)
assert.Equal(t, tc.level, level)
})
}
}

View File

@@ -14,7 +14,6 @@ import (
"github.com/qdm12/gluetun/internal/provider" "github.com/qdm12/gluetun/internal/provider"
"github.com/qdm12/gluetun/internal/routing" "github.com/qdm12/gluetun/internal/routing"
"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"
"github.com/qdm12/golibs/os" "github.com/qdm12/golibs/os"
) )
@@ -45,7 +44,7 @@ type looper struct {
logger, pfLogger logging.Logger logger, pfLogger logging.Logger
client *http.Client client *http.Client
openFile os.OpenFileFunc openFile os.OpenFileFunc
streamMerger command.StreamMerger tunnelReady chan<- struct{}
cancel context.CancelFunc cancel context.CancelFunc
// Internal channels and locks // Internal channels and locks
loopLock sync.Mutex loopLock sync.Mutex
@@ -63,7 +62,7 @@ func NewLooper(settings settings.OpenVPN,
username string, puid, pgid int, allServers models.AllServers, username string, puid, pgid int, allServers models.AllServers,
conf Configurator, fw firewall.Configurator, routing routing.Routing, conf Configurator, fw firewall.Configurator, routing routing.Routing,
logger logging.Logger, client *http.Client, openFile os.OpenFileFunc, logger logging.Logger, client *http.Client, openFile os.OpenFileFunc,
streamMerger command.StreamMerger, cancel context.CancelFunc) Looper { tunnelReady chan<- struct{}, cancel context.CancelFunc) Looper {
return &looper{ return &looper{
state: state{ state: state{
status: constants.Stopped, status: constants.Stopped,
@@ -80,7 +79,7 @@ func NewLooper(settings settings.OpenVPN,
pfLogger: logger.WithPrefix("port forwarding: "), pfLogger: logger.WithPrefix("port forwarding: "),
client: client, client: client,
openFile: openFile, openFile: openFile,
streamMerger: streamMerger, tunnelReady: tunnelReady,
cancel: cancel, cancel: cancel,
start: make(chan struct{}), start: make(chan struct{}),
running: make(chan models.LoopStatus), running: make(chan models.LoopStatus),
@@ -144,7 +143,7 @@ func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup) {
openvpnCtx, openvpnCancel := context.WithCancel(context.Background()) openvpnCtx, openvpnCancel := context.WithCancel(context.Background())
stream, waitFn, err := l.conf.Start(openvpnCtx) stdoutLines, stderrLines, waitError, err := l.conf.Start(openvpnCtx)
if err != nil { if err != nil {
openvpnCancel() openvpnCancel()
l.signalCrashedStatus() l.signalCrashedStatus()
@@ -152,6 +151,9 @@ func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup) {
continue continue
} }
wg.Add(1)
go l.collectLines(wg, stdoutLines, stderrLines)
// Needs the stream line from main.go to know when the tunnel is up // Needs the stream line from main.go to know when the tunnel is up
go func(ctx context.Context) { go func(ctx context.Context) {
for { for {
@@ -166,13 +168,6 @@ func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup) {
} }
}(openvpnCtx) }(openvpnCtx)
go l.streamMerger.Merge(openvpnCtx, stream, command.MergeName("openvpn"))
waitError := make(chan error)
go func() {
err := waitFn() // blocking
waitError <- err
}()
if l.crashed { if l.crashed {
l.crashed = false l.crashed = false
l.backoffTime = defaultBackoffTime l.backoffTime = defaultBackoffTime
@@ -189,6 +184,8 @@ func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup) {
openvpnCancel() openvpnCancel()
<-waitError <-waitError
close(waitError) close(waitError)
close(stdoutLines)
close(stderrLines)
return return
case <-l.stop: case <-l.stop:
l.logger.Info("stopping") l.logger.Info("stopping")
@@ -207,6 +204,8 @@ func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup) {
} }
} }
close(waitError) close(waitError)
close(stdoutLines)
close(stderrLines)
openvpnCancel() // just for the linter openvpnCancel() // just for the linter
} }
} }

View File

@@ -2,7 +2,6 @@ package openvpn
import ( import (
"context" "context"
"io"
"github.com/qdm12/gluetun/internal/unix" "github.com/qdm12/gluetun/internal/unix"
"github.com/qdm12/golibs/command" "github.com/qdm12/golibs/command"
@@ -15,7 +14,8 @@ type Configurator interface {
WriteAuthFile(user, password string, puid, pgid int) error WriteAuthFile(user, password string, puid, pgid int) error
CheckTUN() error CheckTUN() error
CreateTUN() error CreateTUN() error
Start(ctx context.Context) (stdout io.ReadCloser, waitFn func() error, err error) Start(ctx context.Context) (stdoutLines, stderrLines chan string,
waitError chan error, err error)
} }
type configurator struct { type configurator struct {