Persistent server pools (#226)

* GetAllServers with version & timestamp tests
* Storage package to sync servers
* Use storage Sync to get and use servers
This commit is contained in:
Quentin McGaw
2020-08-25 19:38:50 -04:00
committed by GitHub
parent 6fc2b3dd21
commit aa9693a84d
23 changed files with 464 additions and 83 deletions

View File

@@ -104,5 +104,6 @@ RUN apk add -q --progress --no-cache --update openvpn ca-certificates iptables i
rm -rf /var/cache/apk/* /etc/unbound/* /usr/sbin/unbound-* /etc/tinyproxy/tinyproxy.conf && \ rm -rf /var/cache/apk/* /etc/unbound/* /usr/sbin/unbound-* /etc/tinyproxy/tinyproxy.conf && \
deluser openvpn && \ deluser openvpn && \
deluser tinyproxy && \ deluser tinyproxy && \
deluser unbound deluser unbound && \
mkdir /gluetun
COPY --from=builder /tmp/gobuild/entrypoint /entrypoint COPY --from=builder /tmp/gobuild/entrypoint /entrypoint

View File

@@ -52,6 +52,7 @@ iptables, DNS over TLS, ShadowSocks and Tinyproxy*
```bash ```bash
docker run -d --name gluetun --cap-add=NET_ADMIN \ docker run -d --name gluetun --cap-add=NET_ADMIN \
-e REGION="CA Montreal" -e USER=js89ds7 -e PASSWORD=8fd9s239G \ -e REGION="CA Montreal" -e USER=js89ds7 -e PASSWORD=8fd9s239G \
-v /yourpath:/gluetun \
qmcgaw/private-internet-access qmcgaw/private-internet-access
``` ```

View File

@@ -23,6 +23,7 @@ import (
"github.com/qdm12/gluetun/internal/server" "github.com/qdm12/gluetun/internal/server"
"github.com/qdm12/gluetun/internal/settings" "github.com/qdm12/gluetun/internal/settings"
"github.com/qdm12/gluetun/internal/shadowsocks" "github.com/qdm12/gluetun/internal/shadowsocks"
"github.com/qdm12/gluetun/internal/storage"
"github.com/qdm12/gluetun/internal/tinyproxy" "github.com/qdm12/gluetun/internal/tinyproxy"
"github.com/qdm12/golibs/command" "github.com/qdm12/golibs/command"
"github.com/qdm12/golibs/files" "github.com/qdm12/golibs/files"
@@ -88,6 +89,14 @@ func _main(background context.Context, args []string) int {
fatalOnError(err) fatalOnError(err)
logger.Info(allSettings.String()) logger.Info(allSettings.String())
// TODO run this in a loop or in openvpn to reload from file without restarting
storage := storage.New(logger)
allServers, err := storage.SyncServers(constants.GetAllServers())
if err != nil {
logger.Error(err)
return 1
}
// Should never change // Should never change
uid, gid := allSettings.System.UID, allSettings.System.GID uid, gid := allSettings.System.UID, allSettings.System.GID
@@ -143,7 +152,7 @@ func _main(background context.Context, args []string) int {
wg := &sync.WaitGroup{} wg := &sync.WaitGroup{}
openvpnLooper := openvpn.NewLooper(allSettings.VPNSP, allSettings.OpenVPN, uid, gid, openvpnLooper := openvpn.NewLooper(allSettings.VPNSP, allSettings.OpenVPN, uid, gid, allServers,
ovpnConf, firewallConf, logger, client, fileManager, streamMerger, fatalOnError) ovpnConf, firewallConf, logger, client, fileManager, streamMerger, fatalOnError)
restartOpenvpn := openvpnLooper.Restart restartOpenvpn := openvpnLooper.Restart
portForward := openvpnLooper.PortForward portForward := openvpnLooper.PortForward

View File

@@ -12,6 +12,8 @@ services:
- 8388:8388/udp # Shadowsocks - 8388:8388/udp # Shadowsocks
- 8000:8000/tcp # Built-in HTTP control server - 8000:8000/tcp # Built-in HTTP control server
# command: # command:
volumes:
- /yourpath:/gluetun
environment: environment:
# More variables are available, see the readme table # More variables are available, see the readme table
- VPNSP=private internet access - VPNSP=private internet access

View File

@@ -7,9 +7,11 @@ import (
"net" "net"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/params" "github.com/qdm12/gluetun/internal/params"
"github.com/qdm12/gluetun/internal/provider" "github.com/qdm12/gluetun/internal/provider"
"github.com/qdm12/gluetun/internal/settings" "github.com/qdm12/gluetun/internal/settings"
"github.com/qdm12/gluetun/internal/storage"
"github.com/qdm12/golibs/files" "github.com/qdm12/golibs/files"
"github.com/qdm12/golibs/logging" "github.com/qdm12/golibs/logging"
) )
@@ -54,7 +56,11 @@ func OpenvpnConfig() error {
if err != nil { if err != nil {
return err return err
} }
providerConf := provider.New(allSettings.OpenVPN.Provider.Name) allServers, err := storage.New(logger).SyncServers(constants.GetAllServers())
if err != nil {
return err
}
providerConf := provider.New(allSettings.OpenVPN.Provider.Name, allServers)
connections, err := providerConf.GetOpenVPNConnections(allSettings.OpenVPN.Provider.ServerSelection) connections, err := providerConf.GetOpenVPNConnections(allSettings.OpenVPN.Provider.ServerSelection)
if err != nil { if err != nil {
return err return err

View File

@@ -0,0 +1,49 @@
package constants
import "github.com/qdm12/gluetun/internal/models"
func GetAllServers() (allServers models.AllServers) {
return models.AllServers{
Version: 1, // used for migration of the top level scheme
Cyberghost: models.CyberghostServers{
Version: 1, // model version
Timestamp: 1598236838, // latest takes precedence
Servers: CyberghostServers(),
},
Mullvad: models.MullvadServers{
Version: 1,
Timestamp: 1598236838,
Servers: MullvadServers(),
},
Nordvpn: models.NordvpnServers{
Version: 1,
Timestamp: 1598236838,
Servers: NordvpnServers(),
},
Pia: models.PiaServers{
Version: 1,
Timestamp: 1598236838,
Servers: PIAServers(),
},
Purevpn: models.PurevpnServers{
Version: 1,
Timestamp: 1598236838,
Servers: PurevpnServers(),
},
Surfshark: models.SurfsharkServers{
Version: 1,
Timestamp: 1598236838,
Servers: SurfsharkServers(),
},
Vyprvpn: models.VyprvpnServers{
Version: 1,
Timestamp: 1598236838,
Servers: VyprvpnServers(),
},
Windscribe: models.WindscribeServers{
Version: 1,
Timestamp: 1598236838,
Servers: WindscribeServers(),
},
}
}

View File

@@ -0,0 +1,58 @@
package constants
import (
"crypto/md5" //nolint:gosec
"encoding/base64"
"encoding/json"
"fmt"
"testing"
"github.com/qdm12/gluetun/internal/models"
"github.com/stretchr/testify/assert"
)
func digestServerModelVersion(t *testing.T, server interface{}, version uint16) string { //nolint:unparam
bytes, err := json.Marshal(server)
if err != nil {
t.Fatal(err)
}
bytes = append(bytes, []byte(fmt.Sprintf("%d", version))...)
arr := md5.Sum(bytes) //nolint:gosec
return base64.RawStdEncoding.EncodeToString(arr[:])
}
func Test_versions(t *testing.T) {
t.Parallel()
allServers := GetAllServers()
assert.Equal(t, "e8eLGRpb1sNX8mDNPOjA6g", digestServerModelVersion(t, models.CyberghostServer{}, allServers.Cyberghost.Version))
assert.Equal(t, "Dk7lO6AbS46VdHJKQb5Wxg", digestServerModelVersion(t, models.MullvadServer{}, allServers.Mullvad.Version))
assert.Equal(t, "fjzfUqJH0KvetGRdZYEtOg", digestServerModelVersion(t, models.NordvpnServer{}, allServers.Nordvpn.Version))
assert.Equal(t, "gYO+bJZCtQvxVk2dTi5d5Q", digestServerModelVersion(t, models.PIAServer{}, allServers.Pia.Version))
assert.Equal(t, "EZ/SBXQOCS/iJU7A9yc7vg", digestServerModelVersion(t, models.PurevpnServer{}, allServers.Purevpn.Version))
assert.Equal(t, "7yfMpHwzRpEngA/6nYsNag", digestServerModelVersion(t, models.SurfsharkServer{}, allServers.Surfshark.Version))
assert.Equal(t, "7yfMpHwzRpEngA/6nYsNag", digestServerModelVersion(t, models.VyprvpnServer{}, allServers.Vyprvpn.Version))
assert.Equal(t, "7yfMpHwzRpEngA/6nYsNag", digestServerModelVersion(t, models.WindscribeServer{}, allServers.Windscribe.Version))
}
func digestServersTimestamp(t *testing.T, servers interface{}, timestamp int64) string { //nolint:unparam
bytes, err := json.Marshal(servers)
if err != nil {
t.Fatal(err)
}
bytes = append(bytes, []byte(fmt.Sprintf("%d", timestamp))...)
arr := md5.Sum(bytes) //nolint:gosec
return base64.RawStdEncoding.EncodeToString(arr[:])
}
func Test_timestamps(t *testing.T) {
t.Parallel()
allServers := GetAllServers()
assert.Equal(t, "lZa+3P5DGuo9VXlsXsW5Jw", digestServersTimestamp(t, allServers.Cyberghost.Servers, allServers.Cyberghost.Timestamp))
assert.Equal(t, "cK5eeY2KU+doigSAonCfVQ", digestServersTimestamp(t, allServers.Mullvad.Servers, allServers.Mullvad.Timestamp))
assert.Equal(t, "ZfMT6wXJJBAT0fOqx3TuOA", digestServersTimestamp(t, allServers.Nordvpn.Servers, allServers.Nordvpn.Timestamp))
assert.Equal(t, "JGzrjRLM5MlWUjAkjpqKWw", digestServersTimestamp(t, allServers.Pia.Servers, allServers.Pia.Timestamp))
assert.Equal(t, "IW1gWNvYTSRDxpAv4kwmzg", digestServersTimestamp(t, allServers.Purevpn.Servers, allServers.Purevpn.Timestamp))
assert.Equal(t, "f934tXGfEVeNGT3TUdnpxw", digestServersTimestamp(t, allServers.Surfshark.Servers, allServers.Surfshark.Timestamp))
assert.Equal(t, "wwkmrCGEW06x7ze8+FO2hg", digestServersTimestamp(t, allServers.Vyprvpn.Servers, allServers.Vyprvpn.Timestamp))
assert.Equal(t, "jT4WjRKNpYojILLJWzGRRw", digestServersTimestamp(t, allServers.Windscribe.Servers, allServers.Windscribe.Timestamp))
}

View File

@@ -2,9 +2,9 @@ package constants
const ( const (
// Announcement is a message announcement // Announcement is a message announcement
Announcement = "Video of the Git history of Gluetun (2020 is crazy): https://youtu.be/khipOYJtGJ0" Announcement = "Persistent server IP addresses at /gluetun/servers.json, please BIND MOUNT"
// AnnouncementExpiration is the expiration date of the announcement in format yyyy-mm-dd // AnnouncementExpiration is the expiration date of the announcement in format yyyy-mm-dd
AnnouncementExpiration = "2020-07-30" AnnouncementExpiration = "2020-09-30"
) )
const ( const (

52
internal/models/server.go Normal file
View File

@@ -0,0 +1,52 @@
package models
import "net"
type PIAServer struct {
IPs []net.IP `json:"ips"`
Region string `json:"region"`
}
type MullvadServer struct {
IPs []net.IP `json:"ips"`
Country string `json:"country"`
City string `json:"city"`
ISP string `json:"isp"`
Owned bool `json:"owned"`
}
type WindscribeServer struct {
Region string `json:"region"`
IPs []net.IP `json:"ips"`
}
type SurfsharkServer struct {
Region string `json:"region"`
IPs []net.IP `json:"ips"`
}
type CyberghostServer struct {
Region string `json:"region"`
Group string `json:"group"`
IPs []net.IP `json:"ips"`
}
type VyprvpnServer struct {
Region string `json:"region"`
IPs []net.IP `json:"ips"`
}
type NordvpnServer struct { //nolint:maligned
Region string `json:"region"`
Number uint16 `json:"number"`
IP net.IP `json:"ip"`
TCP bool `json:"tcp"`
UDP bool `json:"udp"`
}
type PurevpnServer struct {
Region string `json:"region"`
Country string `json:"country"`
City string `json:"city"`
IPs []net.IP `json:"ips"`
}

View File

@@ -1,52 +1,54 @@
package models package models
import "net" type AllServers struct {
Version uint16 `json:"version"`
type PIAServer struct { Cyberghost CyberghostServers `json:"cyberghost"`
IPs []net.IP Mullvad MullvadServers `json:"mullvad"`
Region string Nordvpn NordvpnServers `json:"nordvpn"`
Pia PiaServers `json:"pia"`
Purevpn PurevpnServers `json:"purevpn"`
Surfshark SurfsharkServers `json:"surfshark"`
Vyprvpn VyprvpnServers `json:"vyprvpn"`
Windscribe WindscribeServers `json:"windscribe"`
} }
type MullvadServer struct { type CyberghostServers struct {
IPs []net.IP Version uint16 `json:"version"`
Country string Timestamp int64 `json:"timestamp"`
City string Servers []CyberghostServer `json:"servers"`
ISP string
Owned bool
} }
type MullvadServers struct {
type WindscribeServer struct { Version uint16 `json:"version"`
Region string Timestamp int64 `json:"timestamp"`
IPs []net.IP Servers []MullvadServer `json:"servers"`
} }
type NordvpnServers struct {
type SurfsharkServer struct { Version uint16 `json:"version"`
Region string Timestamp int64 `json:"timestamp"`
IPs []net.IP Servers []NordvpnServer `json:"servers"`
} }
type PiaServers struct {
type CyberghostServer struct { Version uint16 `json:"version"`
Region string Timestamp int64 `json:"timestamp"`
Group string Servers []PIAServer `json:"servers"`
IPs []net.IP
} }
type PurevpnServers struct {
type VyprvpnServer struct { Version uint16 `json:"version"`
Region string Timestamp int64 `json:"timestamp"`
IPs []net.IP Servers []PurevpnServer `json:"purevpn"`
} }
type SurfsharkServers struct {
type NordvpnServer struct { //nolint:maligned Version uint16 `json:"version"`
Region string Timestamp int64 `json:"timestamp"`
Number uint16 Servers []SurfsharkServer `json:"servers"`
IP net.IP
TCP bool
UDP bool
} }
type VyprvpnServers struct {
type PurevpnServer struct { Version uint16 `json:"version"`
Region string Timestamp int64 `json:"timestamp"`
Country string Servers []VyprvpnServer `json:"servers"`
City string }
IPs []net.IP type WindscribeServers struct {
Version uint16 `json:"version"`
Timestamp int64 `json:"timestamp"`
Servers []WindscribeServer `json:"servers"`
} }

View File

@@ -36,6 +36,7 @@ type looper struct {
// Fixed parameters // Fixed parameters
uid int uid int
gid int gid int
allServers models.AllServers
// Configurators // Configurators
conf Configurator conf Configurator
fw firewall.Configurator fw firewall.Configurator
@@ -51,7 +52,7 @@ type looper struct {
} }
func NewLooper(provider models.VPNProvider, settings settings.OpenVPN, func NewLooper(provider models.VPNProvider, settings settings.OpenVPN,
uid, gid int, uid, gid int, allServers models.AllServers,
conf Configurator, fw firewall.Configurator, conf Configurator, fw firewall.Configurator,
logger logging.Logger, client network.Client, fileManager files.FileManager, logger logging.Logger, client network.Client, fileManager files.FileManager,
streamMerger command.StreamMerger, fatalOnError func(err error)) Looper { streamMerger command.StreamMerger, fatalOnError func(err error)) Looper {
@@ -60,6 +61,7 @@ func NewLooper(provider models.VPNProvider, settings settings.OpenVPN,
settings: settings, settings: settings,
uid: uid, uid: uid,
gid: gid, gid: gid,
allServers: allServers,
conf: conf, conf: conf,
fw: fw, fw: fw,
logger: logger.WithPrefix("openvpn: "), logger: logger.WithPrefix("openvpn: "),
@@ -99,7 +101,7 @@ func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup) {
for ctx.Err() == nil { for ctx.Err() == nil {
settings := l.GetSettings() settings := l.GetSettings()
providerConf := provider.New(l.provider) providerConf := provider.New(l.provider, l.allServers)
connections, err := providerConf.GetOpenVPNConnections(settings.Provider.ServerSelection) connections, err := providerConf.GetOpenVPNConnections(settings.Provider.ServerSelection)
if err != nil { if err != nil {
l.fatalOnError(err) l.fatalOnError(err)

View File

@@ -9,10 +9,14 @@ import (
"github.com/qdm12/golibs/network" "github.com/qdm12/golibs/network"
) )
type cyberghost struct{} type cyberghost struct {
servers []models.CyberghostServer
}
func newCyberghost() *cyberghost { func newCyberghost(servers []models.CyberghostServer) *cyberghost {
return &cyberghost{} return &cyberghost{
servers: servers,
}
} }
func (c *cyberghost) filterServers(region, group string) (servers []models.CyberghostServer) { func (c *cyberghost) filterServers(region, group string) (servers []models.CyberghostServer) {

View File

@@ -9,10 +9,14 @@ import (
"github.com/qdm12/golibs/network" "github.com/qdm12/golibs/network"
) )
type mullvad struct{} type mullvad struct {
servers []models.MullvadServer
}
func newMullvad() *mullvad { func newMullvad(servers []models.MullvadServer) *mullvad {
return &mullvad{} return &mullvad{
servers: servers,
}
} }
func (m *mullvad) filterServers(country, city, isp string) (servers []models.MullvadServer) { func (m *mullvad) filterServers(country, city, isp string) (servers []models.MullvadServer) {

View File

@@ -9,10 +9,14 @@ import (
"github.com/qdm12/golibs/network" "github.com/qdm12/golibs/network"
) )
type nordvpn struct{} type nordvpn struct {
servers []models.NordvpnServer
}
func newNordvpn() *nordvpn { func newNordvpn(servers []models.NordvpnServer) *nordvpn {
return &nordvpn{} return &nordvpn{
servers: servers,
}
} }
func (n *nordvpn) filterServers(region string, protocol models.NetworkProtocol, number uint16) (servers []models.NordvpnServer) { func (n *nordvpn) filterServers(region string, protocol models.NetworkProtocol, number uint16) (servers []models.NordvpnServer) {

View File

@@ -15,11 +15,13 @@ import (
type pia struct { type pia struct {
random random.Random random random.Random
servers []models.PIAServer
} }
func newPrivateInternetAccess() *pia { func newPrivateInternetAccess(servers []models.PIAServer) *pia {
return &pia{ return &pia{
random: random.NewRandom(), random: random.NewRandom(),
servers: servers,
} }
} }

View File

@@ -13,24 +13,24 @@ type Provider interface {
GetPortForward(client network.Client) (port uint16, err error) GetPortForward(client network.Client) (port uint16, err error)
} }
func New(provider models.VPNProvider) Provider { func New(provider models.VPNProvider, allServers models.AllServers) Provider {
switch provider { switch provider {
case constants.PrivateInternetAccess: case constants.PrivateInternetAccess:
return newPrivateInternetAccess() return newPrivateInternetAccess(allServers.Pia.Servers)
case constants.Mullvad: case constants.Mullvad:
return newMullvad() return newMullvad(allServers.Mullvad.Servers)
case constants.Windscribe: case constants.Windscribe:
return newWindscribe() return newWindscribe(allServers.Windscribe.Servers)
case constants.Surfshark: case constants.Surfshark:
return newSurfshark() return newSurfshark(allServers.Surfshark.Servers)
case constants.Cyberghost: case constants.Cyberghost:
return newCyberghost() return newCyberghost(allServers.Cyberghost.Servers)
case constants.Vyprvpn: case constants.Vyprvpn:
return newVyprvpn() return newVyprvpn(allServers.Vyprvpn.Servers)
case constants.Nordvpn: case constants.Nordvpn:
return newNordvpn() return newNordvpn(allServers.Nordvpn.Servers)
case constants.Purevpn: case constants.Purevpn:
return newPurevpn() return newPurevpn(allServers.Purevpn.Servers)
default: default:
return nil // should never occur return nil // should never occur
} }

View File

@@ -9,10 +9,14 @@ import (
"github.com/qdm12/golibs/network" "github.com/qdm12/golibs/network"
) )
type purevpn struct{} type purevpn struct {
servers []models.PurevpnServer
}
func newPurevpn() *purevpn { func newPurevpn(servers []models.PurevpnServer) *purevpn {
return &purevpn{} return &purevpn{
servers: servers,
}
} }
func (p *purevpn) filterServers(region, country, city string) (servers []models.PurevpnServer) { func (p *purevpn) filterServers(region, country, city string) (servers []models.PurevpnServer) {

View File

@@ -9,10 +9,14 @@ import (
"github.com/qdm12/golibs/network" "github.com/qdm12/golibs/network"
) )
type surfshark struct{} type surfshark struct {
servers []models.SurfsharkServer
}
func newSurfshark() *surfshark { func newSurfshark(servers []models.SurfsharkServer) *surfshark {
return &surfshark{} return &surfshark{
servers: servers,
}
} }
func (s *surfshark) filterServers(region string) (servers []models.SurfsharkServer) { func (s *surfshark) filterServers(region string) (servers []models.SurfsharkServer) {

View File

@@ -9,10 +9,14 @@ import (
"github.com/qdm12/golibs/network" "github.com/qdm12/golibs/network"
) )
type vyprvpn struct{} type vyprvpn struct {
servers []models.VyprvpnServer
}
func newVyprvpn() *vyprvpn { func newVyprvpn(servers []models.VyprvpnServer) *vyprvpn {
return &vyprvpn{} return &vyprvpn{
servers: servers,
}
} }
func (v *vyprvpn) filterServers(region string) (servers []models.VyprvpnServer) { func (v *vyprvpn) filterServers(region string) (servers []models.VyprvpnServer) {

View File

@@ -9,10 +9,14 @@ import (
"github.com/qdm12/golibs/network" "github.com/qdm12/golibs/network"
) )
type windscribe struct{} type windscribe struct {
servers []models.WindscribeServer
}
func newWindscribe() *windscribe { func newWindscribe(servers []models.WindscribeServer) *windscribe {
return &windscribe{} return &windscribe{
servers: servers,
}
} }
func (w *windscribe) filterServers(region string) (servers []models.WindscribeServer) { func (w *windscribe) filterServers(region string) (servers []models.WindscribeServer) {

68
internal/storage/merge.go Normal file
View File

@@ -0,0 +1,68 @@
package storage
import (
"time"
"github.com/qdm12/gluetun/internal/models"
)
func getUnixTimeDifference(unix1, unix2 int64) (difference time.Duration) {
difference = time.Unix(unix1, 0).Sub(time.Unix(unix2, 0))
if difference < 0 {
difference = -difference
}
return difference.Truncate(time.Second)
}
func (s *storage) mergeServers(hardcoded, persistent models.AllServers) (merged models.AllServers) {
merged.Version = hardcoded.Version
merged.Cyberghost = hardcoded.Cyberghost
if persistent.Cyberghost.Timestamp > hardcoded.Cyberghost.Timestamp {
s.logger.Info("Using Cyberghost servers from file (%s more recent)",
getUnixTimeDifference(persistent.Cyberghost.Timestamp, hardcoded.Cyberghost.Timestamp))
merged.Cyberghost = persistent.Cyberghost
}
merged.Mullvad = hardcoded.Mullvad
if persistent.Mullvad.Timestamp > hardcoded.Mullvad.Timestamp {
s.logger.Info("Using Mullvad servers from file (%s more recent)",
getUnixTimeDifference(persistent.Mullvad.Timestamp, hardcoded.Mullvad.Timestamp))
merged.Mullvad = persistent.Mullvad
}
merged.Nordvpn = hardcoded.Nordvpn
if persistent.Nordvpn.Timestamp > hardcoded.Nordvpn.Timestamp {
s.logger.Info("Using Nordvpn servers from file (%s more recent)",
getUnixTimeDifference(persistent.Nordvpn.Timestamp, hardcoded.Nordvpn.Timestamp))
merged.Nordvpn = persistent.Nordvpn
}
merged.Pia = hardcoded.Pia
if persistent.Pia.Timestamp > hardcoded.Pia.Timestamp {
s.logger.Info("Using Private Internet Access servers from file (%s more recent)",
getUnixTimeDifference(persistent.Pia.Timestamp, hardcoded.Pia.Timestamp))
merged.Pia = persistent.Pia
}
merged.Purevpn = hardcoded.Purevpn
if persistent.Purevpn.Timestamp > hardcoded.Purevpn.Timestamp {
s.logger.Info("Using Purevpn servers from file (%s more recent)",
getUnixTimeDifference(persistent.Purevpn.Timestamp, hardcoded.Purevpn.Timestamp))
merged.Purevpn = persistent.Purevpn
}
merged.Surfshark = hardcoded.Surfshark
if persistent.Surfshark.Timestamp > hardcoded.Surfshark.Timestamp {
s.logger.Info("Using Surfshark servers from file (%s more recent)",
getUnixTimeDifference(persistent.Surfshark.Timestamp, hardcoded.Surfshark.Timestamp))
merged.Surfshark = persistent.Surfshark
}
merged.Vyprvpn = hardcoded.Vyprvpn
if persistent.Vyprvpn.Timestamp > hardcoded.Vyprvpn.Timestamp {
s.logger.Info("Using Vyprvpn servers from file (%s more recent)",
getUnixTimeDifference(persistent.Vyprvpn.Timestamp, hardcoded.Vyprvpn.Timestamp))
merged.Vyprvpn = persistent.Vyprvpn
}
merged.Windscribe = hardcoded.Windscribe
if persistent.Windscribe.Timestamp > hardcoded.Windscribe.Timestamp {
s.logger.Info("Using Windscribe servers from file (%s more recent)",
getUnixTimeDifference(persistent.Windscribe.Timestamp, hardcoded.Windscribe.Timestamp))
merged.Windscribe = persistent.Windscribe
}
return merged
}

View File

@@ -0,0 +1,29 @@
package storage
import (
"io/ioutil"
"os"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/logging"
)
type Storage interface {
SyncServers(hardcodedServers models.AllServers) (allServers models.AllServers, err error)
}
type storage struct {
osStat func(name string) (os.FileInfo, error)
readFile func(filename string) (data []byte, err error)
writeFile func(filename string, data []byte, perm os.FileMode) error
logger logging.Logger
}
func New(logger logging.Logger) Storage {
return &storage{
osStat: os.Stat,
readFile: ioutil.ReadFile,
writeFile: ioutil.WriteFile,
logger: logger.WithPrefix("storage: "),
}
}

72
internal/storage/sync.go Normal file
View File

@@ -0,0 +1,72 @@
package storage
import (
"encoding/json"
"fmt"
"os"
"reflect"
"github.com/qdm12/gluetun/internal/models"
)
const (
jsonFilepath = "/gluetun/servers.json"
)
func countServers(allServers models.AllServers) int {
return len(allServers.Cyberghost.Servers) +
len(allServers.Mullvad.Servers) +
len(allServers.Nordvpn.Servers) +
len(allServers.Pia.Servers) +
len(allServers.Purevpn.Servers) +
len(allServers.Surfshark.Servers) +
len(allServers.Vyprvpn.Servers) +
len(allServers.Windscribe.Servers)
}
func (s *storage) SyncServers(hardcodedServers models.AllServers) (allServers models.AllServers, err error) {
// Eventually read file
var serversOnFile models.AllServers
_, err = s.osStat(jsonFilepath)
if err == nil {
serversOnFile, err = s.readFromFile()
if err != nil {
return allServers, err
}
} else if !os.IsNotExist(err) {
return allServers, err
}
// Merge data from file and hardcoded
s.logger.Info("Merging by most recent %d hardcoded servers and %d servers read from %s",
countServers(hardcodedServers), countServers(serversOnFile), jsonFilepath)
allServers = s.mergeServers(hardcodedServers, serversOnFile)
// Eventually write file
if reflect.DeepEqual(serversOnFile, allServers) {
return allServers, nil
}
return allServers, s.flushToFile(allServers)
}
func (s *storage) readFromFile() (servers models.AllServers, err error) {
bytes, err := s.readFile(jsonFilepath)
if err != nil {
return servers, err
}
if err := json.Unmarshal(bytes, &servers); err != nil {
return servers, err
}
return servers, nil
}
func (s *storage) flushToFile(servers models.AllServers) error {
bytes, err := json.MarshalIndent(servers, "", " ")
if err != nil {
return fmt.Errorf("cannot write to file: %w", err)
}
if err := s.writeFile(jsonFilepath, bytes, 0644); err != nil {
return err
}
return nil
}