This commit is contained in:
Quentin McGaw
2020-08-30 14:48:57 +00:00
parent aac5274eab
commit 7c102c0028
11 changed files with 262 additions and 18 deletions

View File

@@ -38,6 +38,7 @@ LABEL \
org.opencontainers.image.title="VPN client for PIA, Mullvad, Windscribe, Surfshark and Cyberghost" \ org.opencontainers.image.title="VPN client for PIA, Mullvad, Windscribe, Surfshark and Cyberghost" \
org.opencontainers.image.description="VPN client to tunnel to PIA, Mullvad, Windscribe, Surfshark and Cyberghost servers using OpenVPN, IPtables, DNS over TLS and Alpine Linux" org.opencontainers.image.description="VPN client to tunnel to PIA, Mullvad, Windscribe, Surfshark and Cyberghost servers using OpenVPN, IPtables, DNS over TLS and Alpine Linux"
ENV VPNSP=pia \ ENV VPNSP=pia \
VERSION_INFORMATION=on \
PROTOCOL=udp \ PROTOCOL=udp \
OPENVPN_VERBOSITY=1 \ OPENVPN_VERBOSITY=1 \
OPENVPN_ROOT=no \ OPENVPN_ROOT=no \

View File

@@ -253,6 +253,7 @@ That one is important if you want to connect to the container from your LAN for
| Variable | Default | Choices | Description | | Variable | Default | Choices | Description |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `PUBLICIP_PERIOD` | `12h` | Valid duration | Period to check for public IP address. Set to `0` to disable. | | `PUBLICIP_PERIOD` | `12h` | Valid duration | Period to check for public IP address. Set to `0` to disable. |
| `VERSION_INFORMATION` | `on` | `on`, `off` | Logs a message indicating if a newer version is available once the VPN is connected |
## Connect to it ## Connect to it

View File

@@ -3,6 +3,7 @@ package main
import ( import (
"context" "context"
"fmt" "fmt"
"net/http"
"os" "os"
"os/signal" "os/signal"
"strings" "strings"
@@ -25,6 +26,7 @@ import (
"github.com/qdm12/gluetun/internal/shadowsocks" "github.com/qdm12/gluetun/internal/shadowsocks"
"github.com/qdm12/gluetun/internal/storage" "github.com/qdm12/gluetun/internal/storage"
"github.com/qdm12/gluetun/internal/tinyproxy" "github.com/qdm12/gluetun/internal/tinyproxy"
versionpkg "github.com/qdm12/gluetun/internal/version"
"github.com/qdm12/golibs/command" "github.com/qdm12/golibs/command"
"github.com/qdm12/golibs/files" "github.com/qdm12/golibs/files"
"github.com/qdm12/golibs/logging" "github.com/qdm12/golibs/logging"
@@ -220,6 +222,18 @@ func _main(background context.Context, args []string) int { //nolint:gocognit,go
restartShadowsocks() restartShadowsocks()
} }
versionInformation := func() {
if !allSettings.VersionInformation {
return
}
client := &http.Client{Timeout: 5 * time.Second}
message, err := versionpkg.GetMessage(version, commit, client)
if err != nil {
logger.Error(err)
return
}
logger.Info(message)
}
go func() { go func() {
var restartTickerContext context.Context var restartTickerContext context.Context
var restartTickerCancel context.CancelFunc = func() {} var restartTickerCancel context.CancelFunc = func() {}
@@ -232,7 +246,7 @@ func _main(background context.Context, args []string) int { //nolint:gocognit,go
restartTickerCancel() restartTickerCancel()
restartTickerContext, restartTickerCancel = context.WithCancel(ctx) restartTickerContext, restartTickerCancel = context.WithCancel(ctx)
go unboundLooper.RunRestartTicker(restartTickerContext) go unboundLooper.RunRestartTicker(restartTickerContext)
onConnected(allSettings, logger, routingConf, portForward, restartUnbound, restartPublicIP) onConnected(allSettings, logger, routingConf, portForward, restartUnbound, restartPublicIP, versionInformation)
} }
} }
}() }()
@@ -336,7 +350,7 @@ func collectStreamLines(ctx context.Context, streamMerger command.StreamMerger,
} }
func onConnected(allSettings settings.Settings, logger logging.Logger, routingConf routing.Routing, func onConnected(allSettings settings.Settings, logger logging.Logger, routingConf routing.Routing,
portForward, restartUnbound, restartPublicIP func(), portForward, restartUnbound, restartPublicIP, versionInformation func(),
) { ) {
restartUnbound() restartUnbound()
restartPublicIP() restartPublicIP()
@@ -354,4 +368,5 @@ func onConnected(allSettings settings.Settings, logger logging.Logger, routingCo
logger.Info("Gateway VPN IP address: %s", vpnGatewayIP) logger.Info("Gateway VPN IP address: %s", vpnGatewayIP)
} }
} }
versionInformation()
} }

View File

@@ -108,6 +108,8 @@ type Reader interface {
// Public IP getters // Public IP getters
GetPublicIPPeriod() (period time.Duration, err error) GetPublicIPPeriod() (period time.Duration, err error)
GetVersionInformation() (enabled bool, err error)
} }
type reader struct { type reader struct {
@@ -138,3 +140,7 @@ func (r *reader) GetVPNSP() (vpnServiceProvider models.VPNProvider, err error) {
} }
return models.VPNProvider(s), err return models.VPNProvider(s), err
} }
func (r *reader) GetVersionInformation() (enabled bool, err error) {
return r.envParams.GetOnOff("VERSION_INFORMATION", libparams.Default("on"))
}

View File

@@ -31,10 +31,6 @@ type DNS struct {
} }
func (d *DNS) String() string { func (d *DNS) String() string {
const (
enabled = "enabled"
disabled = "disabled"
)
if !d.Enabled { if !d.Enabled {
return fmt.Sprintf("DNS over TLS disabled, using plaintext DNS %s", d.PlaintextAddress) return fmt.Sprintf("DNS over TLS disabled, using plaintext DNS %s", d.PlaintextAddress)
} }

View File

@@ -8,6 +8,11 @@ import (
"github.com/qdm12/gluetun/internal/params" "github.com/qdm12/gluetun/internal/params"
) )
const (
enabled = "enabled"
disabled = "disabled"
)
// Settings contains all settings for the program to run // Settings contains all settings for the program to run
type Settings struct { type Settings struct {
VPNSP models.VPNProvider VPNSP models.VPNProvider
@@ -18,9 +23,14 @@ type Settings struct {
TinyProxy TinyProxy TinyProxy TinyProxy
ShadowSocks ShadowSocks ShadowSocks ShadowSocks
PublicIPPeriod time.Duration PublicIPPeriod time.Duration
VersionInformation bool
} }
func (s *Settings) String() string { func (s *Settings) String() string {
versionInformation := disabled
if s.VersionInformation {
versionInformation = enabled
}
return strings.Join([]string{ return strings.Join([]string{
"Settings summary below:", "Settings summary below:",
s.OpenVPN.String(), s.OpenVPN.String(),
@@ -30,6 +40,7 @@ func (s *Settings) String() string {
s.TinyProxy.String(), s.TinyProxy.String(),
s.ShadowSocks.String(), s.ShadowSocks.String(),
"Public IP check period: " + s.PublicIPPeriod.String(), "Public IP check period: " + s.PublicIPPeriod.String(),
"Version information: " + versionInformation,
"", // new line at the end "", // new line at the end
}, "\n") }, "\n")
} }
@@ -69,5 +80,9 @@ func GetAllSettings(paramsReader params.Reader) (settings Settings, err error) {
if err != nil { if err != nil {
return settings, err return settings, err
} }
settings.VersionInformation, err = paramsReader.GetVersionInformation()
if err != nil {
return settings, err
}
return settings, nil return settings, nil
} }

View File

@@ -20,9 +20,9 @@ func (s *ShadowSocks) String() string {
if !s.Enabled { if !s.Enabled {
return "ShadowSocks settings: disabled" return "ShadowSocks settings: disabled"
} }
log := "disabled" log := disabled
if s.Log { if s.Log {
log = "enabled" log = enabled
} }
settingsList := []string{ settingsList := []string{
"ShadowSocks settings:", "ShadowSocks settings:",

View File

@@ -21,9 +21,9 @@ func (t *TinyProxy) String() string {
if !t.Enabled { if !t.Enabled {
return "TinyProxy settings: disabled" return "TinyProxy settings: disabled"
} }
auth := "disabled" auth := disabled
if t.User != "" { if t.User != "" {
auth = "enabled" auth = enabled
} }
settingsList := []string{ settingsList := []string{
fmt.Sprintf("Port: %d", t.Port), fmt.Sprintf("Port: %d", t.Port),

View File

@@ -0,0 +1,58 @@
package version
import (
"encoding/json"
"io/ioutil"
"net/http"
"time"
)
type githubRelease struct {
TagName string `json:"tag_name"`
Name string `json:"name"`
Prerelease bool `json:"prerelease"`
PublishedAt time.Time `json:"published_at"`
}
type githubCommit struct {
Sha string `json:"sha"`
Commit struct {
Committer struct {
Date time.Time `json:"date"`
} `json:"committer"`
}
}
func getGithubReleases(client *http.Client) (releases []githubRelease, err error) {
const url = "https://api.github.com/repos/qdm12/gluetun/releases"
response, err := client.Get(url)
if err != nil {
return nil, err
}
defer response.Body.Close()
b, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, err
}
if err := json.Unmarshal(b, &releases); err != nil {
return nil, err
}
return releases, nil
}
func getGithubCommits(client *http.Client) (commits []githubCommit, err error) {
const url = "https://api.github.com/repos/qdm12/gluetun/commits"
response, err := client.Get(url)
if err != nil {
return nil, err
}
defer response.Body.Close()
b, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, err
}
if err := json.Unmarshal(b, &commits); err != nil {
return nil, err
}
return commits, nil
}

View File

@@ -0,0 +1,88 @@
package version
import (
"fmt"
"net/http"
"time"
)
// GetMessage returns a message for the user describing if there is a newer version
// available. It should only be called once the tunnel is established.
func GetMessage(version, commitShort string, client *http.Client) (message string, err error) {
if version == "latest" {
// Find # of commits between current commit and latest commit
commitsSince, err := getCommitsSince(client, commitShort)
if err != nil {
return "", fmt.Errorf("cannot get version information: %w", err)
} else if commitsSince == 0 {
return fmt.Sprintf("You are running on the bleeding edge of %s!", version), nil
}
commits := "commits"
if commitsSince == 1 {
commits = "commit"
}
return fmt.Sprintf("You are running %d %s behind the most recent %s", commitsSince, commits, version), nil
}
tagName, name, releaseTime, err := getLatestRelease(client)
if err != nil {
return "", fmt.Errorf("cannot get version information: %w", err)
}
if tagName == version {
return fmt.Sprintf("You are running the latest release %s", version), nil
}
timeSinceRelease := formatDuration(time.Since(releaseTime))
return fmt.Sprintf("There is a new release %s (%s) created %s ago",
tagName, name, timeSinceRelease),
nil
}
func formatDuration(duration time.Duration) string {
switch {
case duration < time.Minute:
seconds := int(duration.Round(time.Second).Seconds())
if seconds < 2 {
return fmt.Sprintf("%d second", seconds)
}
return fmt.Sprintf("%d seconds", seconds)
case duration <= time.Hour:
minutes := int(duration.Round(time.Minute).Minutes())
if minutes == 1 {
return "1 minute"
}
return fmt.Sprintf("%d minutes", minutes)
case duration < 48*time.Hour:
hours := int(duration.Truncate(time.Hour).Hours())
return fmt.Sprintf("%d hours", hours)
default:
days := int(duration.Truncate(time.Hour).Hours() / 24)
return fmt.Sprintf("%d days", days)
}
}
func getLatestRelease(client *http.Client) (tagName, name string, time time.Time, err error) {
releases, err := getGithubReleases(client)
if err != nil {
return "", "", time, err
}
for _, release := range releases {
if release.Prerelease {
continue
}
return release.TagName, release.Name, release.PublishedAt, nil
}
return "", "", time, fmt.Errorf("no releases found")
}
func getCommitsSince(client *http.Client, commitShort string) (n int, err error) {
commits, err := getGithubCommits(client)
if err != nil {
return 0, err
}
for i := range commits {
if commits[i].Sha[:7] == commitShort {
return n, nil
}
n++
}
return 0, fmt.Errorf("no commit matching %q was found", commitShort)
}

View File

@@ -0,0 +1,64 @@
package version
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func Test_formatDuration(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
duration time.Duration
s string
}{
"zero": {
s: "0 second",
},
"one second": {
duration: time.Second,
s: "1 second",
},
"59 seconds": {
duration: 59 * time.Second,
s: "59 seconds",
},
"1 minute": {
duration: time.Minute,
s: "1 minute",
},
"2 minutes": {
duration: 2 * time.Minute,
s: "2 minutes",
},
"1 hour": {
duration: time.Hour,
s: "60 minutes",
},
"2 hours": {
duration: 2 * time.Hour,
s: "2 hours",
},
"26 hours": {
duration: 26 * time.Hour,
s: "26 hours",
},
"28 hours": {
duration: 28 * time.Hour,
s: "28 hours",
},
"55 hours": {
duration: 55 * time.Hour,
s: "2 days",
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
s := formatDuration(testCase.duration)
assert.Equal(t, testCase.s, s)
})
}
}