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 # Gluetun VPN client
*Lightweight swiss-knife-like VPN client to tunnel to Private Internet Access, *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* iptables, DNS over TLS, ShadowSocks and Tinyproxy*
**ANNOUNCEMENT**: *[Video of the Git history of Gluetun](https://youtu.be/khipOYJtGJ0)* **ANNOUNCEMENT**: *[Video of the Git history of Gluetun](https://youtu.be/khipOYJtGJ0)*
@@ -35,8 +35,7 @@ iptables, DNS over TLS, ShadowSocks and Tinyproxy*
## Features ## Features
- Based on Alpine 3.12 for a small Docker image of 52MB - Based on Alpine 3.12 for a small Docker image of 52MB
- Supports **Private Internet Access**, **Mullvad**, **Windscribe**, - Supports **Private Internet Access**, **Mullvad**, **Windscribe**, **Surfshark**, **Cyberghost**, **Vyprvpn** and **NordVPN** servers
**Surfshark**, **Cyberghost** and **NordVPN** servers
- DNS over TLS baked in with service provider(s) of your choice - 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 - 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` - 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) - [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 🎆 - 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/) - **Private Internet Access**: pick the [region](https://www.privateinternetaccess.com/pages/network/), the level of encryption and enable port forwarding
- Pick the level of encryption - **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))
- Enable port forwarding - **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
### Mullvad - **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
- Pick the [country, city and ISP](https://mullvad.net/en/servers/#openvpn) - **NordVPN**: Pick the region and optionally the server number
- 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)
### Extra niche features ### 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`. And use the line produced as the value for the environment variable `CLIENT_KEY`.
- NordVPN - Vyprvpn
| Variable | Default | Choices | Description | | Variable | Default | Choices | Description |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| 🏁 `USER` | | | Your username | | 🏁 `USER` | | | Your username |
| 🏁 `PASSWORD` | | | Your password | | 🏁 `PASSWORD` | | | Your password |
| `REGION` | `Austria` | One of the [VyprVPN regions](https://www.vyprvpn.com/server-locations) | VPN server region | | `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 - NordVPN
@@ -227,7 +207,8 @@ Want more testing? ▶ [see the Wiki](https://github.com/qdm12/private-internet-
| --- | --- | --- | --- | | --- | --- | --- | --- |
| 🏁 `USER` | | | Your username | | 🏁 `USER` | | | Your username |
| 🏁 `PASSWORD` | | | Your password | | 🏁 `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 ### 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 | | `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 | | `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 ## Connect to it
There are various ways to achieve this, depending on your use case. 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 // wait for restartUnbound
go unboundLooper.Run(ctx, wg) 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 restartPublicIP := publicIPLooper.Restart
setPublicIPPeriod := publicIPLooper.SetPeriod
go publicIPLooper.Run(ctx) go publicIPLooper.Run(ctx)
go publicIPLooper.RunRestartTicker(ctx) go publicIPLooper.RunRestartTicker(ctx)
setPublicIPPeriod(allSettings.PublicIPPeriod) // call after RunRestartTicker
tinyproxyLooper := tinyproxy.NewLooper(tinyProxyConf, firewallConf, allSettings.TinyProxy, logger, streamMerger, uid, gid) tinyproxyLooper := tinyproxy.NewLooper(tinyProxyConf, firewallConf, allSettings.TinyProxy, logger, streamMerger, uid, gid)
restartTinyproxy := tinyproxyLooper.Restart restartTinyproxy := tinyproxyLooper.Restart

View File

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

View File

@@ -5,6 +5,7 @@ import (
"regexp" "regexp"
"strings" "strings"
"github.com/fatih/color"
"github.com/qdm12/golibs/logging" "github.com/qdm12/golibs/logging"
"github.com/qdm12/private-internet-access-docker/internal/constants" "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) { func PostProcessLine(s string) (filtered string, level logging.Level) {
switch { switch {
case strings.HasPrefix(s, "openvpn: "): case strings.HasPrefix(s, "openvpn: "):
filtered = constants.ColorOpenvpn().Sprintf(s) for _, ignored := range []string{
return filtered, logging.InfoLevel "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: "): case strings.HasPrefix(s, "unbound: "):
prefix := regularExpressions.unboundPrefix.FindString(s) prefix := regularExpressions.unboundPrefix.FindString(s)
filtered = s[len(prefix):] 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: BLABLA Jul 12 23:07:25 [32]: Reloading config file",
"tinyproxy: Reloading config file", "tinyproxy: Reloading config file",
logging.ErrorLevel}, 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 { for name, tc := range tests {
tc := tc tc := tc

View File

@@ -21,12 +21,15 @@ type Looper interface {
Run(ctx context.Context, wg *sync.WaitGroup) Run(ctx context.Context, wg *sync.WaitGroup)
Restart() Restart()
PortForward() PortForward()
GetSettings() (settings settings.OpenVPN)
SetSettings(settings settings.OpenVPN)
} }
type looper struct { type looper struct {
// Variable parameters // Variable parameters
provider models.VPNProvider provider models.VPNProvider
settings settings.OpenVPN settings settings.OpenVPN
settingsMutex sync.RWMutex
// Fixed parameters // Fixed parameters
uid int uid int
gid 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) Restart() { l.restart <- struct{}{} }
func (l *looper) PortForward() { l.portForwardSignals <- 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) { func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup) {
wg.Add(1) wg.Add(1)
defer wg.Done() defer wg.Done()
@@ -80,23 +95,24 @@ func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup) {
defer l.logger.Warn("loop exited") defer l.logger.Warn("loop exited")
for ctx.Err() == nil { for ctx.Err() == nil {
settings := l.GetSettings()
providerConf := provider.New(l.provider) providerConf := provider.New(l.provider)
connections, err := providerConf.GetOpenVPNConnections(l.settings.Provider.ServerSelection) connections, err := providerConf.GetOpenVPNConnections(settings.Provider.ServerSelection)
l.fatalOnError(err) l.fatalOnError(err)
lines := providerConf.BuildConf( lines := providerConf.BuildConf(
connections, connections,
l.settings.Verbosity, settings.Verbosity,
l.uid, l.uid,
l.gid, l.gid,
l.settings.Root, settings.Root,
l.settings.Cipher, settings.Cipher,
l.settings.Auth, settings.Auth,
l.settings.Provider.ExtraConfigOptions, settings.Provider.ExtraConfigOptions,
) )
err = l.fileManager.WriteLinesToFile(string(constants.OpenVPNConf), lines, files.Ownership(l.uid, l.gid), files.Permissions(0400)) err = l.fileManager.WriteLinesToFile(string(constants.OpenVPNConf), lines, files.Ownership(l.uid, l.gid), files.Permissions(0400))
l.fatalOnError(err) 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) l.fatalOnError(err)
if err := l.fw.SetVPNConnections(ctx, connections); err != nil { 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) { 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 return
} }
var port uint16 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) 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) l.logger.Info("writing forwarded port to %s", filepath)
err = l.fileManager.WriteLinesToFile( err = l.fileManager.WriteLinesToFile(
string(filepath), []string{fmt.Sprintf("%d", port)}, string(filepath), []string{fmt.Sprintf("%d", port)},

View File

@@ -99,6 +99,9 @@ type Reader interface {
GetTinyProxyUser() (user string, err error) GetTinyProxyUser() (user string, err error)
GetTinyProxyPassword() (password string, err error) GetTinyProxyPassword() (password string, err error)
// Public IP getters
GetPublicIPPeriod() (period time.Duration, err error)
// Version getters // Version getters
GetVersion() string GetVersion() string
GetBuildDate() 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 ( import (
"context" "context"
"sync"
"time" "time"
"github.com/qdm12/golibs/files" "github.com/qdm12/golibs/files"
@@ -14,9 +15,14 @@ type Looper interface {
Run(ctx context.Context) Run(ctx context.Context)
RunRestartTicker(ctx context.Context) RunRestartTicker(ctx context.Context)
Restart() Restart()
Stop()
GetPeriod() (period time.Duration)
SetPeriod(period time.Duration)
} }
type looper struct { type looper struct {
period time.Duration
periodMutex sync.RWMutex
getter IPGetter getter IPGetter
logger logging.Logger logger logging.Logger
fileManager files.FileManager fileManager files.FileManager
@@ -24,11 +30,14 @@ type looper struct {
uid int uid int
gid int gid int
restart chan struct{} restart chan struct{}
stop chan struct{}
updateTicker chan struct{}
} }
func NewLooper(client network.Client, logger logging.Logger, fileManager files.FileManager, 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{ return &looper{
period: period,
getter: NewIPGetter(client), getter: NewIPGetter(client),
logger: logger.WithPrefix("ip getter: "), logger: logger.WithPrefix("ip getter: "),
fileManager: fileManager, fileManager: fileManager,
@@ -36,10 +45,26 @@ func NewLooper(client network.Client, logger logging.Logger, fileManager files.F
uid: uid, uid: uid,
gid: gid, gid: gid,
restart: make(chan struct{}), restart: make(chan struct{}),
stop: make(chan struct{}),
updateTicker: make(chan struct{}),
} }
} }
func (l *looper) Restart() { l.restart <- 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) { func (l *looper) logAndWait(ctx context.Context, err error) {
l.logger.Error(err) l.logger.Error(err)
@@ -57,7 +82,23 @@ func (l *looper) Run(ctx context.Context) {
} }
defer l.logger.Warn("loop exited") defer l.logger.Warn("loop exited")
enabled := true
for ctx.Err() == nil { 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() ip, err := l.getter.Get()
if err != nil { if err != nil {
l.logAndWait(ctx, err) l.logAndWait(ctx, err)
@@ -75,8 +116,9 @@ func (l *looper) Run(ctx context.Context) {
} }
select { select {
case <-l.restart: // triggered restart case <-l.restart: // triggered restart
case <-l.stop:
enabled = false
case <-ctx.Done(): case <-ctx.Done():
l.logger.Warn("context canceled: exiting loop")
return return
} }
} }
@@ -84,6 +126,12 @@ func (l *looper) Run(ctx context.Context) {
func (l *looper) RunRestartTicker(ctx context.Context) { func (l *looper) RunRestartTicker(ctx context.Context) {
ticker := time.NewTicker(time.Hour) ticker := time.NewTicker(time.Hour)
period := l.GetPeriod()
if period > 0 {
ticker = time.NewTicker(period)
} else {
ticker.Stop()
}
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
@@ -91,6 +139,9 @@ func (l *looper) RunRestartTicker(ctx context.Context) {
return return
case <-ticker.C: case <-ticker.C:
l.restart <- struct{}{} l.restart <- struct{}{}
case <-l.updateTicker:
ticker.Stop()
ticker = time.NewTicker(l.GetPeriod())
} }
} }
} }

View File

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

View File

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

View File

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