Compare commits

..

13 Commits

Author SHA1 Message Date
Quentin McGaw
b4a4e441c1 Fix #199 when ticker period is 0 2020-07-16 12:00:25 +00:00
Quentin McGaw
e8526141be Fix issues in readme 2020-07-16 01:52:09 +00:00
Quentin McGaw
9abb630692 Get and set settings for DNS 2020-07-16 01:45:05 +00:00
Quentin McGaw
9b92ece5a1 Fix race condition for public ip loop 2020-07-16 01:44:48 +00:00
Quentin McGaw
87a3e54044 Set and get settings for openvpn 2020-07-16 01:26:37 +00:00
Quentin McGaw
76b730e2a6 Improve openvpn logging
- Show Initialization Sequence completed in green
- Show all other openvpn logs in the openvpn color
2020-07-16 01:20:47 +00:00
Quentin McGaw
51af8d1ab0 PUBLICIP_PERIOD environment variable 2020-07-16 01:12:54 +00:00
Quentin McGaw
002ffacd35 Shadowsocks get and set settings 2020-07-16 00:05:00 +00:00
Quentin McGaw
404cee9371 Tinyproxy set and get settings 2020-07-16 00:05:00 +00:00
Quentin McGaw
f89e7aa8dc Update readme list of VPN providers supported 2020-07-16 00:04:58 +00:00
Quentin McGaw
a0312ec916 Shadowsocks and Tinyproxy Start and Stop 2020-07-16 00:04:15 +00:00
Quentin McGaw
83cf59b93e Start and Stop for dns over tls 2020-07-16 00:04:15 +00:00
Quentin McGaw
ad5de13c25 Logging filtering for openvpn process 2020-07-16 00:04:14 +00:00
12 changed files with 524 additions and 173 deletions

View File

@@ -1,7 +1,7 @@
# Gluetun VPN client
*Lightweight swiss-knife-like VPN client to tunnel to Private Internet Access,
Mullvad, Windscribe, Surfshark Cyberghost and NordVPN VPN servers, using Go, OpenVPN,
Mullvad, Windscribe, Surfshark Cyberghost, VyprVPN and NordVPN VPN servers, using Go, OpenVPN,
iptables, DNS over TLS, ShadowSocks and Tinyproxy*
**ANNOUNCEMENT**: *[Video of the Git history of Gluetun](https://youtu.be/khipOYJtGJ0)*
@@ -35,8 +35,7 @@ iptables, DNS over TLS, ShadowSocks and Tinyproxy*
## Features
- Based on Alpine 3.12 for a small Docker image of 52MB
- Supports **Private Internet Access**, **Mullvad**, **Windscribe**,
**Surfshark**, **Cyberghost** and **NordVPN** servers
- Supports **Private Internet Access**, **Mullvad**, **Windscribe**, **Surfshark**, **Cyberghost**, **Vyprvpn** and **NordVPN** servers
- DNS over TLS baked in with service provider(s) of your choice
- DNS fine blocking of malicious/ads/surveillance hostnames and IP addresses, with live update every 24 hours
- Choose the vpn network protocol, `udp` or `tcp`
@@ -47,33 +46,15 @@ iptables, DNS over TLS, ShadowSocks and Tinyproxy*
- [Connect LAN devices to it](https://github.com/qdm12/private-internet-access-docker#connect-to-it)
- Compatible with amd64, i686 (32 bit), **ARM** 64 bit, ARM 32 bit v6 and v7 🎆
### Private Internet Access
### VPN provider specifics
- Pick the [region](https://www.privateinternetaccess.com/pages/network/)
- Pick the level of encryption
- Enable port forwarding
### Mullvad
- Pick the [country, city and ISP](https://mullvad.net/en/servers/#openvpn)
- Pick the port to use (i.e. `53` (udp) or `80` (tcp))
### Windscribe
- Pick the [region](https://windscribe.com/status)
- Pick the port to use
### Surfshark
- Pick the [region](https://github.com/qdm12/private-internet-access-docker/wiki/Surfshark) or a multi hop region name
### Cyberghost
- Pick the [region](https://github.com/qdm12/private-internet-access-docker/wiki/Cyberghost)
### Vyprvpn
- Pick the [region](https://www.vyprvpn.com/server-locations)
- **Private Internet Access**: pick the [region](https://www.privateinternetaccess.com/pages/network/), the level of encryption and enable port forwarding
- **Mullvad**: Pick the [country, city and ISP](https://mullvad.net/en/servers/#openvpn) and optionally a custom port to use (i.e. `53` (udp) or `80` (tcp))
- **Windscribe**: Pick the [region](https://windscribe.com/status), and optionally a custom port to use
- **Surfshark**: Pick the [region](https://github.com/qdm12/private-internet-access-docker/wiki/Surfshark) or a multi hop region name
- **Cyberghost**: Pick the [region](https://github.com/qdm12/private-internet-access-docker/wiki/Cyberghost) and server group.
- **VyprVPN**: Pick the [region](https://www.vyprvpn.com/server-locations), port forwarding works by default
- **NordVPN**: Pick the region and optionally the server number
### Extra niche features
@@ -212,14 +193,13 @@ Want more testing? ▶ [see the Wiki](https://github.com/qdm12/private-internet-
And use the line produced as the value for the environment variable `CLIENT_KEY`.
- NordVPN
- Vyprvpn
| Variable | Default | Choices | Description |
| --- | --- | --- | --- |
| 🏁 `USER` | | | Your username |
| 🏁 `PASSWORD` | | | Your password |
| `REGION` | `Austria` | One of the [VyprVPN regions](https://www.vyprvpn.com/server-locations) | VPN server region |
| `SERVER_NUMBER` | | Server integer number | Optional server number. For example `251` for `Italy #251` |
- NordVPN
@@ -227,7 +207,8 @@ Want more testing? ▶ [see the Wiki](https://github.com/qdm12/private-internet-
| --- | --- | --- | --- |
| 🏁 `USER` | | | Your username |
| 🏁 `PASSWORD` | | | Your password |
| 🏁 `REGION` | `Austria` (wrong) | One of the NordVPN server name, i.e. `Cyprus #12` | VPN server name |
| 🏁 `REGION` | `Austria` (wrong) | One of the NordVPN server country, i.e. `Switzerland` | VPN server country |
| `SERVER_NUMBER` | | Server integer number | Optional server number. For example `251` for `Italy #251` |
### DNS over TLS
@@ -289,6 +270,12 @@ That one is important if you want to connect to the container from your LAN for
| `UID` | `1000` | | User ID to run as non root and for ownership of files written |
| `GID` | `1000` | | Group ID to run as non root and for ownership of files written |
### Other
| Variable | Default | Choices | Description |
| --- | --- | --- | --- |
| `PUBLICIP_PERIOD` | `12h` | Valid duration | Period to check for public IP address. Set to `0` to disable. |
## Connect to it
There are various ways to achieve this, depending on your use case.

View File

@@ -137,10 +137,12 @@ func _main(background context.Context, args []string) int {
// wait for restartUnbound
go unboundLooper.Run(ctx, wg)
publicIPLooper := publicip.NewLooper(client, logger, fileManager, allSettings.System.IPStatusFilepath, uid, gid)
publicIPLooper := publicip.NewLooper(client, logger, fileManager, allSettings.System.IPStatusFilepath, allSettings.PublicIPPeriod, uid, gid)
restartPublicIP := publicIPLooper.Restart
setPublicIPPeriod := publicIPLooper.SetPeriod
go publicIPLooper.Run(ctx)
go publicIPLooper.RunRestartTicker(ctx)
setPublicIPPeriod(allSettings.PublicIPPeriod) // call after RunRestartTicker
tinyproxyLooper := tinyproxy.NewLooper(tinyProxyConf, firewallConf, allSettings.TinyProxy, logger, streamMerger, uid, gid)
restartTinyproxy := tinyproxyLooper.Restart

View File

@@ -16,16 +16,24 @@ type Looper interface {
Run(ctx context.Context, wg *sync.WaitGroup)
RunRestartTicker(ctx context.Context)
Restart()
Start()
Stop()
GetSettings() (settings settings.DNS)
SetSettings(settings settings.DNS)
}
type looper struct {
conf Configurator
settings settings.DNS
settingsMutex sync.RWMutex
logger logging.Logger
streamMerger command.StreamMerger
uid int
gid int
restart chan struct{}
start chan struct{}
stop chan struct{}
updateTicker chan struct{}
}
func NewLooper(conf Configurator, settings settings.DNS, logger logging.Logger,
@@ -38,10 +46,44 @@ func NewLooper(conf Configurator, settings settings.DNS, logger logging.Logger,
gid: gid,
streamMerger: streamMerger,
restart: make(chan struct{}),
start: make(chan struct{}),
stop: make(chan struct{}),
updateTicker: make(chan struct{}),
}
}
func (l *looper) Restart() { l.restart <- struct{}{} }
func (l *looper) Start() { l.start <- struct{}{} }
func (l *looper) Stop() { l.stop <- struct{}{} }
func (l *looper) GetSettings() (settings settings.DNS) {
l.settingsMutex.RLock()
defer l.settingsMutex.RUnlock()
return l.settings
}
func (l *looper) SetSettings(settings settings.DNS) {
l.settingsMutex.Lock()
defer l.settingsMutex.Unlock()
updatePeriodDiffers := l.settings.UpdatePeriod != settings.UpdatePeriod
l.settings = settings
l.settingsMutex.Unlock()
if updatePeriodDiffers {
l.updateTicker <- struct{}{}
}
}
func (l *looper) isEnabled() bool {
l.settingsMutex.RLock()
defer l.settingsMutex.RUnlock()
return l.settings.Enabled
}
func (l *looper) setEnabled(enabled bool) {
l.settingsMutex.Lock()
defer l.settingsMutex.Unlock()
l.settings.Enabled = enabled
}
func (l *looper) logAndWait(ctx context.Context, err error) {
l.logger.Warn(err)
@@ -55,28 +97,44 @@ func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup) {
wg.Add(1)
defer wg.Done()
l.fallbackToUnencryptedDNS()
waitForStart := true
for waitForStart {
select {
case <-l.stop:
l.logger.Info("not started yet")
case <-l.restart:
waitForStart = false
case <-l.start:
waitForStart = false
case <-ctx.Done():
return
}
}
defer l.logger.Warn("loop exited")
var unboundCtx context.Context
var unboundCancel context.CancelFunc = func() {}
var waitError chan error
triggeredRestart := false
l.setEnabled(true)
for ctx.Err() == nil {
if !l.settings.Enabled {
// wait for another restart signal to recheck if it is enabled
for !l.isEnabled() {
// wait for a signal to re-enable
select {
case <-l.stop:
l.logger.Info("already disabled")
case <-l.restart:
l.setEnabled(true)
case <-l.start:
l.setEnabled(true)
case <-ctx.Done():
unboundCancel()
return
}
}
settings := l.GetSettings()
// Setup
if err := l.conf.DownloadRootHints(l.uid, l.gid); err != nil {
l.logAndWait(ctx, err)
@@ -86,7 +144,7 @@ func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup) {
l.logAndWait(ctx, err)
continue
}
if err := l.conf.MakeUnboundConf(l.settings, l.uid, l.gid); err != nil {
if err := l.conf.MakeUnboundConf(settings, l.uid, l.gid); err != nil {
l.logAndWait(ctx, err)
continue
}
@@ -98,7 +156,7 @@ func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup) {
close(waitError)
}
unboundCtx, unboundCancel = context.WithCancel(context.Background())
stream, waitFn, err := l.conf.Start(unboundCtx, l.settings.VerbosityDetailsLevel)
stream, waitFn, err := l.conf.Start(unboundCtx, settings.VerbosityDetailsLevel)
if err != nil {
unboundCancel()
l.fallbackToUnencryptedDNS()
@@ -109,7 +167,7 @@ func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup) {
// Started successfully
go l.streamMerger.Merge(unboundCtx, stream, command.MergeName("unbound"))
l.conf.UseDNSInternally(net.IP{127, 0, 0, 1}) // use Unbound
if err := l.conf.UseDNSSystemWide(net.IP{127, 0, 0, 1}, l.settings.KeepNameserver); err != nil { // use Unbound
if err := l.conf.UseDNSSystemWide(net.IP{127, 0, 0, 1}, settings.KeepNameserver); err != nil { // use Unbound
l.logger.Error(err)
}
if err := l.conf.WaitForUnbound(); err != nil {
@@ -124,7 +182,8 @@ func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup) {
waitError <- err
}()
// Wait for one of the three cases below
stayHere := true
for stayHere {
select {
case <-ctx.Done():
l.logger.Warn("context canceled: exiting loop")
@@ -136,36 +195,50 @@ func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup) {
l.logger.Info("restarting")
// unboundCancel occurs next loop run when the setup is complete
triggeredRestart = true
stayHere = false
case <-l.start:
l.logger.Info("already started")
case <-l.stop:
l.logger.Info("stopping")
unboundCancel()
<-waitError
close(waitError)
l.setEnabled(false)
stayHere = false
case err := <-waitError: // unexpected error
close(waitError)
unboundCancel()
l.fallbackToUnencryptedDNS()
l.logAndWait(ctx, err)
stayHere = false
}
}
}
unboundCancel()
}
func (l *looper) fallbackToUnencryptedDNS() {
settings := l.GetSettings()
// Try with user provided plaintext ip address
targetIP := l.settings.PlaintextAddress
targetIP := settings.PlaintextAddress
if targetIP != nil {
l.logger.Info("falling back on plaintext DNS at address %s", targetIP)
l.conf.UseDNSInternally(targetIP)
if err := l.conf.UseDNSSystemWide(targetIP, l.settings.KeepNameserver); err != nil {
if err := l.conf.UseDNSSystemWide(targetIP, settings.KeepNameserver); err != nil {
l.logger.Error(err)
}
return
}
// Try with any IPv4 address from the providers chosen
for _, provider := range l.settings.Providers {
for _, provider := range settings.Providers {
data := constants.DNSProviderMapping()[provider]
for _, targetIP = range data.IPs {
if targetIP.To4() != nil {
l.logger.Info("falling back on plaintext DNS at address %s", targetIP)
l.conf.UseDNSInternally(targetIP)
if err := l.conf.UseDNSSystemWide(targetIP, l.settings.KeepNameserver); err != nil {
if err := l.conf.UseDNSSystemWide(targetIP, settings.KeepNameserver); err != nil {
l.logger.Error(err)
}
return
@@ -174,14 +247,17 @@ func (l *looper) fallbackToUnencryptedDNS() {
}
// No IPv4 address found
l.logger.Error("no ipv4 DNS address found for providers %s", l.settings.Providers)
l.logger.Error("no ipv4 DNS address found for providers %s", settings.Providers)
}
func (l *looper) RunRestartTicker(ctx context.Context) {
if l.settings.UpdatePeriod == 0 {
return
ticker := time.NewTicker(time.Hour)
settings := l.GetSettings()
if settings.UpdatePeriod > 0 {
ticker = time.NewTicker(settings.UpdatePeriod)
} else {
ticker.Stop()
}
ticker := time.NewTicker(l.settings.UpdatePeriod)
for {
select {
case <-ctx.Done():
@@ -189,6 +265,10 @@ func (l *looper) RunRestartTicker(ctx context.Context) {
return
case <-ticker.C:
l.restart <- struct{}{}
case <-l.updateTicker:
ticker.Stop()
period := l.GetSettings().UpdatePeriod
ticker = time.NewTicker(period)
}
}
}

View File

@@ -5,6 +5,7 @@ import (
"regexp"
"strings"
"github.com/fatih/color"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/private-internet-access-docker/internal/constants"
)
@@ -26,8 +27,35 @@ var regularExpressions = struct { //nolint:gochecknoglobals
func PostProcessLine(s string) (filtered string, level logging.Level) {
switch {
case strings.HasPrefix(s, "openvpn: "):
filtered = constants.ColorOpenvpn().Sprintf(s)
return filtered, logging.InfoLevel
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
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):]

View File

@@ -80,6 +80,30 @@ func Test_PostProcessLine(t *testing.T) {
"tinyproxy: BLABLA Jul 12 23:07:25 [32]: Reloading config file",
"tinyproxy: Reloading config file",
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},
}
for name, tc := range tests {
tc := tc

View File

@@ -21,12 +21,15 @@ type Looper interface {
Run(ctx context.Context, wg *sync.WaitGroup)
Restart()
PortForward()
GetSettings() (settings settings.OpenVPN)
SetSettings(settings settings.OpenVPN)
}
type looper struct {
// Variable parameters
provider models.VPNProvider
settings settings.OpenVPN
settingsMutex sync.RWMutex
// Fixed parameters
uid int
gid int
@@ -69,6 +72,18 @@ func NewLooper(provider models.VPNProvider, settings settings.OpenVPN,
func (l *looper) Restart() { l.restart <- struct{}{} }
func (l *looper) PortForward() { l.portForwardSignals <- struct{}{} }
func (l *looper) GetSettings() (settings settings.OpenVPN) {
l.settingsMutex.RLock()
defer l.settingsMutex.RUnlock()
return l.settings
}
func (l *looper) SetSettings(settings settings.OpenVPN) {
l.settingsMutex.Lock()
defer l.settingsMutex.Unlock()
l.settings = settings
}
func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup) {
wg.Add(1)
defer wg.Done()
@@ -80,23 +95,24 @@ func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup) {
defer l.logger.Warn("loop exited")
for ctx.Err() == nil {
settings := l.GetSettings()
providerConf := provider.New(l.provider)
connections, err := providerConf.GetOpenVPNConnections(l.settings.Provider.ServerSelection)
connections, err := providerConf.GetOpenVPNConnections(settings.Provider.ServerSelection)
l.fatalOnError(err)
lines := providerConf.BuildConf(
connections,
l.settings.Verbosity,
settings.Verbosity,
l.uid,
l.gid,
l.settings.Root,
l.settings.Cipher,
l.settings.Auth,
l.settings.Provider.ExtraConfigOptions,
settings.Root,
settings.Cipher,
settings.Auth,
settings.Provider.ExtraConfigOptions,
)
err = l.fileManager.WriteLinesToFile(string(constants.OpenVPNConf), lines, files.Ownership(l.uid, l.gid), files.Permissions(0400))
l.fatalOnError(err)
err = l.conf.WriteAuthFile(l.settings.User, l.settings.Password, l.uid, l.gid)
err = l.conf.WriteAuthFile(settings.User, settings.Password, l.uid, l.gid)
l.fatalOnError(err)
if err := l.fw.SetVPNConnections(ctx, connections); err != nil {
@@ -158,7 +174,8 @@ func (l *looper) logAndWait(ctx context.Context, err error) {
}
func (l *looper) portForward(ctx context.Context, providerConf provider.Provider, client network.Client) {
if !l.settings.Provider.PortForwarding.Enabled {
settings := l.GetSettings()
if !settings.Provider.PortForwarding.Enabled {
return
}
var port uint16
@@ -175,7 +192,7 @@ func (l *looper) portForward(ctx context.Context, providerConf provider.Provider
l.logger.Info("port forwarded is %d", port)
}
filepath := l.settings.Provider.PortForwarding.Filepath
filepath := settings.Provider.PortForwarding.Filepath
l.logger.Info("writing forwarded port to %s", filepath)
err = l.fileManager.WriteLinesToFile(
string(filepath), []string{fmt.Sprintf("%d", port)},

View File

@@ -99,6 +99,9 @@ type Reader interface {
GetTinyProxyUser() (user string, err error)
GetTinyProxyPassword() (password string, err error)
// Public IP getters
GetPublicIPPeriod() (period time.Duration, err error)
// Version getters
GetVersion() string
GetBuildDate() string

View File

@@ -0,0 +1,17 @@
package params
import (
"time"
libparams "github.com/qdm12/golibs/params"
)
// GetPublicIPPeriod obtains the period to fetch the IP address periodically.
// Set to 0 to disable
func (r *reader) GetPublicIPPeriod() (period time.Duration, err error) {
s, err := r.envParams.GetEnv("PUBLICIP_PERIOD", libparams.Default("12h"))
if err != nil {
return 0, err
}
return time.ParseDuration(s)
}

View File

@@ -2,6 +2,7 @@ package publicip
import (
"context"
"sync"
"time"
"github.com/qdm12/golibs/files"
@@ -14,9 +15,14 @@ type Looper interface {
Run(ctx context.Context)
RunRestartTicker(ctx context.Context)
Restart()
Stop()
GetPeriod() (period time.Duration)
SetPeriod(period time.Duration)
}
type looper struct {
period time.Duration
periodMutex sync.RWMutex
getter IPGetter
logger logging.Logger
fileManager files.FileManager
@@ -24,11 +30,14 @@ type looper struct {
uid int
gid int
restart chan struct{}
stop chan struct{}
updateTicker chan struct{}
}
func NewLooper(client network.Client, logger logging.Logger, fileManager files.FileManager,
ipStatusFilepath models.Filepath, uid, gid int) Looper {
ipStatusFilepath models.Filepath, period time.Duration, uid, gid int) Looper {
return &looper{
period: period,
getter: NewIPGetter(client),
logger: logger.WithPrefix("ip getter: "),
fileManager: fileManager,
@@ -36,10 +45,26 @@ func NewLooper(client network.Client, logger logging.Logger, fileManager files.F
uid: uid,
gid: gid,
restart: make(chan struct{}),
stop: make(chan struct{}),
updateTicker: make(chan struct{}),
}
}
func (l *looper) Restart() { l.restart <- struct{}{} }
func (l *looper) Stop() { l.stop <- struct{}{} }
func (l *looper) GetPeriod() (period time.Duration) {
l.periodMutex.RLock()
defer l.periodMutex.RUnlock()
return l.period
}
func (l *looper) SetPeriod(period time.Duration) {
l.periodMutex.Lock()
l.period = period
l.periodMutex.Unlock()
l.updateTicker <- struct{}{}
}
func (l *looper) logAndWait(ctx context.Context, err error) {
l.logger.Error(err)
@@ -57,7 +82,23 @@ func (l *looper) Run(ctx context.Context) {
}
defer l.logger.Warn("loop exited")
enabled := true
for ctx.Err() == nil {
for !enabled {
// wait for a signal to re-enable
select {
case <-l.stop:
l.logger.Info("already disabled")
case <-l.restart:
enabled = true
case <-ctx.Done():
return
}
}
// Enabled and has a period set
ip, err := l.getter.Get()
if err != nil {
l.logAndWait(ctx, err)
@@ -75,8 +116,9 @@ func (l *looper) Run(ctx context.Context) {
}
select {
case <-l.restart: // triggered restart
case <-l.stop:
enabled = false
case <-ctx.Done():
l.logger.Warn("context canceled: exiting loop")
return
}
}
@@ -84,6 +126,12 @@ func (l *looper) Run(ctx context.Context) {
func (l *looper) RunRestartTicker(ctx context.Context) {
ticker := time.NewTicker(time.Hour)
period := l.GetPeriod()
if period > 0 {
ticker = time.NewTicker(period)
} else {
ticker.Stop()
}
for {
select {
case <-ctx.Done():
@@ -91,6 +139,9 @@ func (l *looper) RunRestartTicker(ctx context.Context) {
return
case <-ticker.C:
l.restart <- struct{}{}
case <-l.updateTicker:
ticker.Stop()
ticker = time.NewTicker(l.GetPeriod())
}
}
}

View File

@@ -2,6 +2,7 @@ package settings
import (
"strings"
"time"
"github.com/qdm12/private-internet-access-docker/internal/models"
"github.com/qdm12/private-internet-access-docker/internal/params"
@@ -16,6 +17,7 @@ type Settings struct {
Firewall Firewall
TinyProxy TinyProxy
ShadowSocks ShadowSocks
PublicIPPeriod time.Duration
}
func (s *Settings) String() string {
@@ -27,6 +29,7 @@ func (s *Settings) String() string {
s.Firewall.String(),
s.TinyProxy.String(),
s.ShadowSocks.String(),
"Public IP check period: " + s.PublicIPPeriod.String(),
"", // new line at the end
}, "\n")
}
@@ -62,5 +65,9 @@ func GetAllSettings(paramsReader params.Reader) (settings Settings, err error) {
if err != nil {
return settings, err
}
settings.PublicIPPeriod, err = paramsReader.GetPublicIPPeriod()
if err != nil {
return settings, err
}
return settings, nil
}

View File

@@ -14,18 +14,25 @@ import (
type Looper interface {
Run(ctx context.Context, wg *sync.WaitGroup)
Restart()
Start()
Stop()
GetSettings() (settings settings.ShadowSocks)
SetSettings(settings settings.ShadowSocks)
}
type looper struct {
conf Configurator
firewallConf firewall.Configurator
settings settings.ShadowSocks
dnsSettings settings.DNS
settingsMutex sync.RWMutex
dnsSettings settings.DNS // TODO
logger logging.Logger
streamMerger command.StreamMerger
uid int
gid int
restart chan struct{}
start chan struct{}
stop chan struct{}
}
func (l *looper) logAndWait(ctx context.Context, err error) {
@@ -48,34 +55,81 @@ func NewLooper(conf Configurator, firewallConf firewall.Configurator, settings s
uid: uid,
gid: gid,
restart: make(chan struct{}),
start: make(chan struct{}),
stop: make(chan struct{}),
}
}
func (l *looper) Restart() { l.restart <- struct{}{} }
func (l *looper) Start() { l.start <- struct{}{} }
func (l *looper) Stop() { l.stop <- struct{}{} }
func (l *looper) GetSettings() (settings settings.ShadowSocks) {
l.settingsMutex.RLock()
defer l.settingsMutex.RUnlock()
return l.settings
}
func (l *looper) SetSettings(settings settings.ShadowSocks) {
l.settingsMutex.Lock()
defer l.settingsMutex.Unlock()
l.settings = settings
}
func (l *looper) isEnabled() bool {
l.settingsMutex.RLock()
defer l.settingsMutex.RUnlock()
return l.settings.Enabled
}
func (l *looper) setEnabled(enabled bool) {
l.settingsMutex.Lock()
defer l.settingsMutex.Unlock()
l.settings.Enabled = enabled
}
func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup) {
wg.Add(1)
defer wg.Done()
waitForStart := true
for waitForStart {
select {
case <-l.stop:
l.logger.Info("not started yet")
case <-l.start:
waitForStart = false
case <-l.restart:
waitForStart = false
case <-ctx.Done():
return
}
}
defer l.logger.Warn("loop exited")
l.setEnabled(true)
var previousPort uint16
for ctx.Err() == nil {
for !l.isEnabled() {
// wait for a signal to re-enable
select {
case <-l.stop:
l.logger.Info("already disabled")
case <-l.restart:
l.setEnabled(true)
case <-l.start:
l.setEnabled(true)
case <-ctx.Done():
return
}
}
nameserver := l.dnsSettings.PlaintextAddress.String()
if l.dnsSettings.Enabled {
nameserver = "127.0.0.1"
}
err := l.conf.MakeConf(
l.settings.Port,
l.settings.Password,
l.settings.Method,
nameserver,
l.uid,
l.gid)
settings := l.GetSettings()
err := l.conf.MakeConf(settings.Port, settings.Password, settings.Method, nameserver, l.uid, l.gid)
if err != nil {
l.logAndWait(ctx, err)
continue
@@ -87,14 +141,14 @@ func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup) {
continue
}
}
if err := l.firewallConf.SetAllowedPort(ctx, l.settings.Port); err != nil {
if err := l.firewallConf.SetAllowedPort(ctx, settings.Port); err != nil {
l.logger.Error(err)
continue
}
previousPort = l.settings.Port
previousPort = settings.Port
shadowsocksCtx, shadowsocksCancel := context.WithCancel(context.Background())
stdout, stderr, waitFn, err := l.conf.Start(shadowsocksCtx, "0.0.0.0", l.settings.Port, l.settings.Password, l.settings.Log)
stdout, stderr, waitFn, err := l.conf.Start(shadowsocksCtx, "0.0.0.0", settings.Port, settings.Password, settings.Log)
if err != nil {
shadowsocksCancel()
l.logAndWait(ctx, err)
@@ -107,6 +161,9 @@ func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup) {
err := waitFn() // blocking
waitError <- err
}()
stayHere := true
for stayHere {
select {
case <-ctx.Done():
l.logger.Warn("context canceled: exiting loop")
@@ -119,10 +176,22 @@ func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup) {
shadowsocksCancel()
<-waitError
close(waitError)
stayHere = false
case <-l.start:
l.logger.Info("already started")
case <-l.stop:
l.logger.Info("stopping")
shadowsocksCancel()
<-waitError
close(waitError)
l.setEnabled(false)
stayHere = false
case err := <-waitError: // unexpected error
shadowsocksCancel()
close(waitError)
l.logAndWait(ctx, err)
}
}
shadowsocksCancel() // repetition for linter only
}
}

View File

@@ -14,17 +14,24 @@ import (
type Looper interface {
Run(ctx context.Context, wg *sync.WaitGroup)
Restart()
Start()
Stop()
GetSettings() (settings settings.TinyProxy)
SetSettings(settings settings.TinyProxy)
}
type looper struct {
conf Configurator
firewallConf firewall.Configurator
settings settings.TinyProxy
settingsMutex sync.RWMutex
logger logging.Logger
streamMerger command.StreamMerger
uid int
gid int
restart chan struct{}
start chan struct{}
stop chan struct{}
}
func (l *looper) logAndWait(ctx context.Context, err error) {
@@ -46,30 +53,75 @@ func NewLooper(conf Configurator, firewallConf firewall.Configurator, settings s
uid: uid,
gid: gid,
restart: make(chan struct{}),
start: make(chan struct{}),
stop: make(chan struct{}),
}
}
func (l *looper) GetSettings() (settings settings.TinyProxy) {
l.settingsMutex.RLock()
defer l.settingsMutex.RUnlock()
return l.settings
}
func (l *looper) SetSettings(settings settings.TinyProxy) {
l.settingsMutex.Lock()
defer l.settingsMutex.Unlock()
l.settings = settings
}
func (l *looper) isEnabled() bool {
l.settingsMutex.RLock()
defer l.settingsMutex.RUnlock()
return l.settings.Enabled
}
func (l *looper) setEnabled(enabled bool) {
l.settingsMutex.Lock()
defer l.settingsMutex.Unlock()
l.settings.Enabled = enabled
}
func (l *looper) Restart() { l.restart <- struct{}{} }
func (l *looper) Start() { l.start <- struct{}{} }
func (l *looper) Stop() { l.stop <- struct{}{} }
func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup) {
wg.Add(1)
defer wg.Done()
waitForStart := true
for waitForStart {
select {
case <-l.stop:
l.logger.Info("not started yet")
case <-l.start:
waitForStart = false
case <-l.restart:
waitForStart = false
case <-ctx.Done():
return
}
}
defer l.logger.Warn("loop exited")
var previousPort uint16
for ctx.Err() == nil {
err := l.conf.MakeConf(
l.settings.LogLevel,
l.settings.Port,
l.settings.User,
l.settings.Password,
l.uid,
l.gid)
for !l.isEnabled() {
// wait for a signal to re-enable
select {
case <-l.stop:
l.logger.Info("already disabled")
case <-l.restart:
l.setEnabled(true)
case <-l.start:
l.setEnabled(true)
case <-ctx.Done():
return
}
}
settings := l.GetSettings()
err := l.conf.MakeConf(settings.LogLevel, settings.Port, settings.User, settings.Password, l.uid, l.gid)
if err != nil {
l.logAndWait(ctx, err)
continue
@@ -81,11 +133,11 @@ func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup) {
continue
}
}
if err := l.firewallConf.SetAllowedPort(ctx, l.settings.Port); err != nil {
if err := l.firewallConf.SetAllowedPort(ctx, settings.Port); err != nil {
l.logger.Error(err)
continue
}
previousPort = l.settings.Port
previousPort = settings.Port
tinyproxyCtx, tinyproxyCancel := context.WithCancel(context.Background())
stream, waitFn, err := l.conf.Start(tinyproxyCtx)
@@ -100,6 +152,8 @@ func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup) {
err := waitFn() // blocking
waitError <- err
}()
stayHere := true
for stayHere {
select {
case <-ctx.Done():
l.logger.Warn("context canceled: exiting loop")
@@ -112,10 +166,22 @@ func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup) {
tinyproxyCancel()
<-waitError
close(waitError)
stayHere = false
case <-l.start:
l.logger.Info("already started")
case <-l.stop:
l.logger.Info("stopping")
tinyproxyCancel()
<-waitError
close(waitError)
l.setEnabled(false)
stayHere = false
case err := <-waitError: // unexpected error
tinyproxyCancel()
close(waitError)
l.logAndWait(ctx, err)
}
}
tinyproxyCancel() // repetition for linter only
}
}