diff --git a/Dockerfile b/Dockerfile index 5e2fa98a..3539a9f5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -164,9 +164,7 @@ ENV VPN_SERVICE_PROVIDER=pia \ # Health HEALTH_SERVER_ADDRESS=127.0.0.1:9999 \ HEALTH_TARGET_ADDRESS=cloudflare.com:443 \ - HEALTH_SUCCESS_WAIT_DURATION=5s \ - HEALTH_VPN_DURATION_INITIAL=6s \ - HEALTH_VPN_DURATION_ADDITION=5s \ + HEALTH_ICMP_TARGET_IP=0.0.0.0 \ # DNS over TLS DOT=on \ DOT_PROVIDERS=cloudflare \ diff --git a/cmd/gluetun/main.go b/cmd/gluetun/main.go index 65e9dbb9..1c80c369 100644 --- a/cmd/gluetun/main.go +++ b/cmd/gluetun/main.go @@ -414,6 +414,13 @@ func _main(ctx context.Context, buildInfo models.BuildInformation, return fmt.Errorf("starting public ip loop: %w", err) } + healthLogger := logger.New(log.SetComponent("healthcheck")) + healthcheckServer := healthcheck.NewServer(allSettings.Health, healthLogger) + healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler( + "HTTP health server", goroutine.OptionTimeout(defaultShutdownTimeout)) + go healthcheckServer.Run(healthServerCtx, healthServerDone) + healthChecker := healthcheck.NewChecker(healthLogger) + updaterLogger := logger.New(log.SetComponent("updater")) unzipper := unzip.New(httpClient) @@ -424,8 +431,8 @@ func _main(ctx context.Context, buildInfo models.BuildInformation, vpnLogger := logger.New(log.SetComponent("vpn")) vpnLooper := vpn.NewLoop(allSettings.VPN, ipv6Supported, allSettings.Firewall.VPNInputPorts, - providers, storage, ovpnConf, netLinker, firewallConf, routingConf, portForwardLooper, - cmder, publicIPLooper, dnsLooper, vpnLogger, httpClient, + providers, storage, allSettings.Health, healthChecker, healthcheckServer, ovpnConf, netLinker, firewallConf, + routingConf, portForwardLooper, cmder, publicIPLooper, dnsLooper, vpnLogger, httpClient, buildInfo, *allSettings.Version.Enabled) vpnHandler, vpnCtx, vpnDone := goshutdown.NewGoRoutineHandler( "vpn", goroutine.OptionTimeout(time.Second)) @@ -476,12 +483,6 @@ func _main(ctx context.Context, buildInfo models.BuildInformation, <-httpServerReady controlGroupHandler.Add(httpServerHandler) - healthLogger := logger.New(log.SetComponent("healthcheck")) - healthcheckServer := healthcheck.NewServer(allSettings.Health, healthLogger, vpnLooper) - healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler( - "HTTP health server", goroutine.OptionTimeout(defaultShutdownTimeout)) - go healthcheckServer.Run(healthServerCtx, healthServerDone) - orderHandler := goshutdown.NewOrderHandler("gluetun", order.OptionTimeout(totalShutdownTimeout), order.OptionOnSuccess(defaultShutdownOnSuccess), diff --git a/go.mod b/go.mod index a841eba7..196f2949 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/qdm12/gluetun go 1.23 require ( - github.com/breml/rootcerts v0.2.19 + github.com/breml/rootcerts v0.2.20 github.com/fatih/color v1.18.0 github.com/golang/mock v1.6.0 github.com/klauspost/compress v1.17.11 diff --git a/go.sum b/go.sum index aa22f727..0751aa27 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/breml/rootcerts v0.2.19 h1:3D/qwAC1xoh82GmZ21mYzQ1NaLOICUVntIo+MRZYr4U= -github.com/breml/rootcerts v0.2.19/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw= +github.com/breml/rootcerts v0.2.20 h1:koth1lShwiiDp3VOX6/4qKEZ87S7HgDKsnDr47XEIq0= +github.com/breml/rootcerts v0.2.20/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= diff --git a/internal/configuration/settings/deprecated.go b/internal/configuration/settings/deprecated.go index 77010781..1e745eb0 100644 --- a/internal/configuration/settings/deprecated.go +++ b/internal/configuration/settings/deprecated.go @@ -9,9 +9,11 @@ import ( func readObsolete(r *reader.Reader) (warnings []string) { keyToMessage := map[string]string{ - "DOT_VERBOSITY": "DOT_VERBOSITY is obsolete, use LOG_LEVEL instead.", - "DOT_VERBOSITY_DETAILS": "DOT_VERBOSITY_DETAILS is obsolete because it was specific to Unbound.", - "DOT_VALIDATION_LOGLEVEL": "DOT_VALIDATION_LOGLEVEL is obsolete because DNSSEC validation is not implemented.", + "DOT_VERBOSITY": "DOT_VERBOSITY is obsolete, use LOG_LEVEL instead.", + "DOT_VERBOSITY_DETAILS": "DOT_VERBOSITY_DETAILS is obsolete because it was specific to Unbound.", + "DOT_VALIDATION_LOGLEVEL": "DOT_VALIDATION_LOGLEVEL is obsolete because DNSSEC validation is not implemented.", + "HEALTH_VPN_DURATION_INITIAL": "HEALTH_VPN_DURATION_INITIAL is obsolete", + "HEALTH_VPN_DURATION_ADDITION": "HEALTH_VPN_DURATION_ADDITION is obsolete", } sortedKeys := maps.Keys(keyToMessage) slices.Sort(sortedKeys) diff --git a/internal/configuration/settings/health.go b/internal/configuration/settings/health.go index 4035e209..c3c263fb 100644 --- a/internal/configuration/settings/health.go +++ b/internal/configuration/settings/health.go @@ -2,6 +2,7 @@ package settings import ( "fmt" + "net/netip" "os" "time" @@ -24,16 +25,13 @@ type Health struct { // HTTP server. It defaults to 500 milliseconds. ReadTimeout time.Duration // TargetAddress is the address (host or host:port) - // to TCP dial to periodically for the health check. + // to TCP TLS dial to periodically for the health check. // It cannot be the empty string in the internal state. TargetAddress string - // SuccessWait is the duration to wait to re-run the - // healthcheck after a successful healthcheck. - // It defaults to 5 seconds and cannot be zero in - // the internal state. - SuccessWait time.Duration - // VPN has health settings specific to the VPN loop. - VPN HealthyWait + // ICMPTargetIP is the IP address to use for ICMP echo requests + // in the health checker. It can be set to an unspecified address + // such that the VPN server IP is used, which is also the default behavior. + ICMPTargetIP netip.Addr } func (h Health) Validate() (err error) { @@ -42,11 +40,6 @@ func (h Health) Validate() (err error) { return fmt.Errorf("server listening address is not valid: %w", err) } - err = h.VPN.validate() - if err != nil { - return fmt.Errorf("health VPN settings: %w", err) - } - return nil } @@ -56,8 +49,7 @@ func (h *Health) copy() (copied Health) { ReadHeaderTimeout: h.ReadHeaderTimeout, ReadTimeout: h.ReadTimeout, TargetAddress: h.TargetAddress, - SuccessWait: h.SuccessWait, - VPN: h.VPN.copy(), + ICMPTargetIP: h.ICMPTargetIP, } } @@ -69,8 +61,7 @@ func (h *Health) OverrideWith(other Health) { h.ReadHeaderTimeout = gosettings.OverrideWithComparable(h.ReadHeaderTimeout, other.ReadHeaderTimeout) h.ReadTimeout = gosettings.OverrideWithComparable(h.ReadTimeout, other.ReadTimeout) h.TargetAddress = gosettings.OverrideWithComparable(h.TargetAddress, other.TargetAddress) - h.SuccessWait = gosettings.OverrideWithComparable(h.SuccessWait, other.SuccessWait) - h.VPN.overrideWith(other.VPN) + h.ICMPTargetIP = gosettings.OverrideWithComparable(h.ICMPTargetIP, other.ICMPTargetIP) } func (h *Health) SetDefaults() { @@ -80,9 +71,7 @@ func (h *Health) SetDefaults() { const defaultReadTimeout = 500 * time.Millisecond h.ReadTimeout = gosettings.DefaultComparable(h.ReadTimeout, defaultReadTimeout) h.TargetAddress = gosettings.DefaultComparable(h.TargetAddress, "cloudflare.com:443") - const defaultSuccessWait = 5 * time.Second - h.SuccessWait = gosettings.DefaultComparable(h.SuccessWait, defaultSuccessWait) - h.VPN.setDefaults() + h.ICMPTargetIP = gosettings.DefaultComparable(h.ICMPTargetIP, netip.IPv4Unspecified()) // use the VPN server IP } func (h Health) String() string { @@ -93,10 +82,11 @@ func (h Health) toLinesNode() (node *gotree.Node) { node = gotree.New("Health settings:") node.Appendf("Server listening address: %s", h.ServerAddress) node.Appendf("Target address: %s", h.TargetAddress) - node.Appendf("Duration to wait after success: %s", h.SuccessWait) - node.Appendf("Read header timeout: %s", h.ReadHeaderTimeout) - node.Appendf("Read timeout: %s", h.ReadTimeout) - node.AppendNode(h.VPN.toLinesNode("VPN")) + icmpTarget := "VPN server IP" + if !h.ICMPTargetIP.IsUnspecified() { + icmpTarget = h.ICMPTargetIP.String() + } + node.Appendf("ICMP target IP: %s", icmpTarget) return node } @@ -104,16 +94,9 @@ func (h *Health) Read(r *reader.Reader) (err error) { h.ServerAddress = r.String("HEALTH_SERVER_ADDRESS") h.TargetAddress = r.String("HEALTH_TARGET_ADDRESS", reader.RetroKeys("HEALTH_ADDRESS_TO_PING")) - - h.SuccessWait, err = r.Duration("HEALTH_SUCCESS_WAIT_DURATION") + h.ICMPTargetIP, err = r.NetipAddr("HEALTH_ICMP_TARGET_IP") if err != nil { return err } - - err = h.VPN.read(r) - if err != nil { - return fmt.Errorf("VPN health settings: %w", err) - } - return nil } diff --git a/internal/configuration/settings/healthywait.go b/internal/configuration/settings/healthywait.go deleted file mode 100644 index e30d610e..00000000 --- a/internal/configuration/settings/healthywait.go +++ /dev/null @@ -1,76 +0,0 @@ -package settings - -import ( - "time" - - "github.com/qdm12/gosettings" - "github.com/qdm12/gosettings/reader" - "github.com/qdm12/gotree" -) - -type HealthyWait struct { - // Initial is the initial duration to wait for the program - // to be healthy before taking action. - // It cannot be nil in the internal state. - Initial *time.Duration - // Addition is the duration to add to the Initial duration - // after Initial has expired to wait longer for the program - // to be healthy. - // It cannot be nil in the internal state. - Addition *time.Duration -} - -func (h HealthyWait) validate() (err error) { - return nil -} - -func (h *HealthyWait) copy() (copied HealthyWait) { - return HealthyWait{ - Initial: gosettings.CopyPointer(h.Initial), - Addition: gosettings.CopyPointer(h.Addition), - } -} - -// overrideWith overrides fields of the receiver -// settings object with any field set in the other -// settings. -func (h *HealthyWait) overrideWith(other HealthyWait) { - h.Initial = gosettings.OverrideWithPointer(h.Initial, other.Initial) - h.Addition = gosettings.OverrideWithPointer(h.Addition, other.Addition) -} - -func (h *HealthyWait) setDefaults() { - const initialDurationDefault = 6 * time.Second - const additionDurationDefault = 5 * time.Second - h.Initial = gosettings.DefaultPointer(h.Initial, initialDurationDefault) - h.Addition = gosettings.DefaultPointer(h.Addition, additionDurationDefault) -} - -func (h HealthyWait) String() string { - return h.toLinesNode("Health").String() -} - -func (h HealthyWait) toLinesNode(kind string) (node *gotree.Node) { - node = gotree.New(kind + " wait durations:") - node.Appendf("Initial duration: %s", *h.Initial) - node.Appendf("Additional duration: %s", *h.Addition) - return node -} - -func (h *HealthyWait) read(r *reader.Reader) (err error) { - h.Initial, err = r.DurationPtr( - "HEALTH_VPN_DURATION_INITIAL", - reader.RetroKeys("HEALTH_OPENVPN_DURATION_INITIAL")) - if err != nil { - return err - } - - h.Addition, err = r.DurationPtr( - "HEALTH_VPN_DURATION_ADDITION", - reader.RetroKeys("HEALTH_OPENVPN_DURATION_ADDITION")) - if err != nil { - return err - } - - return nil -} diff --git a/internal/configuration/settings/settings_test.go b/internal/configuration/settings/settings_test.go index 7aa30a15..955aa349 100644 --- a/internal/configuration/settings/settings_test.go +++ b/internal/configuration/settings/settings_test.go @@ -58,12 +58,7 @@ func Test_Settings_String(t *testing.T) { ├── Health settings: | ├── Server listening address: 127.0.0.1:9999 | ├── Target address: cloudflare.com:443 -| ├── Duration to wait after success: 5s -| ├── Read header timeout: 100ms -| ├── Read timeout: 500ms -| └── VPN wait durations: -| ├── Initial duration: 6s -| └── Additional duration: 5s +| └── ICMP target IP: VPN server IP ├── Shadowsocks server settings: | └── Enabled: no ├── HTTP proxy settings: diff --git a/internal/healthcheck/checker.go b/internal/healthcheck/checker.go new file mode 100644 index 00000000..4775021a --- /dev/null +++ b/internal/healthcheck/checker.go @@ -0,0 +1,239 @@ +package healthcheck + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "net" + "net/netip" + "strings" + "sync" + "time" + + "github.com/qdm12/gluetun/internal/healthcheck/dns" + "github.com/qdm12/gluetun/internal/healthcheck/icmp" +) + +type Checker struct { + tlsDialAddr string + dialer *net.Dialer + echoer *icmp.Echoer + dnsClient *dns.Client + logger Logger + icmpTarget netip.Addr + configMutex sync.Mutex + + icmpNotPermitted bool + + // Internal periodic service signals + stop context.CancelFunc + done <-chan struct{} +} + +func NewChecker(logger Logger) *Checker { + return &Checker{ + dialer: &net.Dialer{ + Resolver: &net.Resolver{ + PreferGo: true, + }, + }, + echoer: icmp.NewEchoer(logger), + dnsClient: dns.New(), + logger: logger, + } +} + +// SetConfig sets the TCP+TLS dial address and the ICMP echo IP address +// to target by the [Checker]. +// This function MUST be called before calling [Checker.Start]. +func (c *Checker) SetConfig(tlsDialAddr string, icmpTarget netip.Addr) { + c.configMutex.Lock() + defer c.configMutex.Unlock() + c.tlsDialAddr = tlsDialAddr + c.icmpTarget = icmpTarget +} + +// Start starts the checker by first running a blocking 2s-timed TCP+TLS check, +// and, on success, starts the periodic checks in a separate goroutine: +// - a "small" ICMP echo check every 15 seconds +// - a "full" TCP+TLS check every 5 minutes +// It returns a channel `runError` that receives an error if one of the periodic checks fail. +// It returns an error if the initial TCP+TLS check fails. +func (c *Checker) Start(ctx context.Context) (runError <-chan error, err error) { + if c.tlsDialAddr == "" || c.icmpTarget.IsUnspecified() { + panic("call Checker.SetConfig with non empty values before Checker.Start") + } + + // connection isn't under load yet when the checker starts, so a short + // 6 seconds timeout suffices and provides quick enough feedback that + // the new connection is not working. + const timeout = 6 * time.Second + tcpTLSCheckCtx, tcpTLSCheckCancel := context.WithTimeout(ctx, timeout) + err = tcpTLSCheck(tcpTLSCheckCtx, c.dialer, c.tlsDialAddr) + tcpTLSCheckCancel() + if err != nil { + return nil, fmt.Errorf("startup check: %w", err) + } + + ready := make(chan struct{}) + ctx, cancel := context.WithCancel(context.Background()) + c.stop = cancel + done := make(chan struct{}) + c.done = done + const smallCheckPeriod = 15 * time.Second + smallCheckTimer := time.NewTimer(smallCheckPeriod) + const fullCheckPeriod = 5 * time.Minute + fullCheckTimer := time.NewTimer(fullCheckPeriod) + runErrorCh := make(chan error) + runError = runErrorCh + go func() { + defer close(done) + close(ready) + for { + select { + case <-ctx.Done(): + fullCheckTimer.Stop() + smallCheckTimer.Stop() + return + case <-smallCheckTimer.C: + err := c.smallPeriodicCheck(ctx) + if err != nil { + runErrorCh <- fmt.Errorf("periodic small check: %w", err) + return + } + smallCheckTimer.Reset(smallCheckPeriod) + case <-fullCheckTimer.C: + err := c.fullPeriodicCheck(ctx) + if err != nil { + runErrorCh <- fmt.Errorf("periodic full check: %w", err) + return + } + fullCheckTimer.Reset(fullCheckPeriod) + } + } + }() + <-ready + return runError, nil +} + +func (c *Checker) Stop() error { + c.stop() + <-c.done + c.icmpTarget = netip.Addr{} + return nil +} + +func (c *Checker) smallPeriodicCheck(ctx context.Context) error { + c.configMutex.Lock() + ip := c.icmpTarget + c.configMutex.Unlock() + const maxTries = 3 + const timeout = 3 * time.Second + const extraTryTime = time.Second // 1s added for each subsequent retry + check := func(ctx context.Context) error { + if c.icmpNotPermitted { + return c.dnsClient.Check(ctx) + } + err := c.echoer.Echo(ctx, ip) + if errors.Is(err, icmp.ErrNotPermitted) { + c.icmpNotPermitted = true + c.logger.Warnf("%s; permanently falling back to plaintext DNS checks.", err) + return c.dnsClient.Check(ctx) + } + return err + } + return withRetries(ctx, maxTries, timeout, extraTryTime, c.logger, "ICMP echo", check) +} + +func (c *Checker) fullPeriodicCheck(ctx context.Context) error { + const maxTries = 2 + // 10s timeout in case the connection is under stress + // See https://github.com/qdm12/gluetun/issues/2270 + const timeout = 10 * time.Second + const extraTryTime = 3 * time.Second // 3s added for each subsequent retry + check := func(ctx context.Context) error { + return tcpTLSCheck(ctx, c.dialer, c.tlsDialAddr) + } + return withRetries(ctx, maxTries, timeout, extraTryTime, c.logger, "TCP+TLS dial", check) +} + +func tcpTLSCheck(ctx context.Context, dialer *net.Dialer, targetAddress string) error { + // TODO use mullvad API if current provider is Mullvad + + address, err := makeAddressToDial(targetAddress) + if err != nil { + return err + } + + const dialNetwork = "tcp4" + connection, err := dialer.DialContext(ctx, dialNetwork, address) + if err != nil { + return fmt.Errorf("dialing: %w", err) + } + + if strings.HasSuffix(address, ":443") { + host, _, err := net.SplitHostPort(address) + if err != nil { + return fmt.Errorf("splitting host and port: %w", err) + } + tlsConfig := &tls.Config{ + MinVersion: tls.VersionTLS12, + ServerName: host, + } + tlsConnection := tls.Client(connection, tlsConfig) + err = tlsConnection.HandshakeContext(ctx) + if err != nil { + return fmt.Errorf("running TLS handshake: %w", err) + } + } + + err = connection.Close() + if err != nil { + return fmt.Errorf("closing connection: %w", err) + } + + return nil +} + +func makeAddressToDial(address string) (addressToDial string, err error) { + host, port, err := net.SplitHostPort(address) + if err != nil { + addrErr := new(net.AddrError) + ok := errors.As(err, &addrErr) + if !ok || addrErr.Err != "missing port in address" { + return "", fmt.Errorf("splitting host and port from address: %w", err) + } + host = address + const defaultPort = "443" + port = defaultPort + } + address = net.JoinHostPort(host, port) + return address, nil +} + +var ErrAllCheckTriesFailed = errors.New("all check tries failed") + +func withRetries(ctx context.Context, maxTries uint, tryTimeout, extraTryTime time.Duration, + warner Logger, checkName string, check func(ctx context.Context) error, +) error { + try := uint(0) + for { + timeout := tryTimeout + time.Duration(try)*extraTryTime //nolint:gosec + checkCtx, cancel := context.WithTimeout(ctx, timeout) + err := check(checkCtx) + cancel() + switch { + case err == nil: + return nil + case ctx.Err() != nil: + return fmt.Errorf("%s context error: %w", checkName, ctx.Err()) + default: + warner.Warnf("%s attempt %d/%d failed: %v", checkName, try+1, maxTries, err) + try++ + if try == maxTries { + return fmt.Errorf("%w: %s: after %d attempts", ErrAllCheckTriesFailed, checkName, maxTries) + } + } + } +} diff --git a/internal/healthcheck/health_test.go b/internal/healthcheck/checker_test.go similarity index 79% rename from internal/healthcheck/health_test.go rename to internal/healthcheck/checker_test.go index 803cb8d0..209be53a 100644 --- a/internal/healthcheck/health_test.go +++ b/internal/healthcheck/checker_test.go @@ -7,12 +7,11 @@ import ( "testing" "time" - "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func Test_Server_healthCheck(t *testing.T) { +func Test_Checker_fullcheck(t *testing.T) { t.Parallel() t.Run("canceled real dialer", func(t *testing.T) { @@ -21,20 +20,18 @@ func Test_Server_healthCheck(t *testing.T) { dialer := &net.Dialer{} const address = "cloudflare.com:443" - server := &Server{ - dialer: dialer, - config: settings.Health{ - TargetAddress: address, - }, + checker := &Checker{ + dialer: dialer, + tlsDialAddr: address, } canceledCtx, cancel := context.WithCancel(context.Background()) cancel() - err := server.healthCheck(canceledCtx) + err := checker.fullPeriodicCheck(canceledCtx) require.Error(t, err) - assert.Contains(t, err.Error(), "operation was canceled") + assert.EqualError(t, err, "TCP+TLS dial context error: context canceled") }) t.Run("dial localhost:0", func(t *testing.T) { @@ -54,14 +51,12 @@ func Test_Server_healthCheck(t *testing.T) { listeningAddress := listener.Addr() dialer := &net.Dialer{} - server := &Server{ - dialer: dialer, - config: settings.Health{ - TargetAddress: listeningAddress.String(), - }, + checker := &Checker{ + dialer: dialer, + tlsDialAddr: listeningAddress.String(), } - err = server.healthCheck(ctx) + err = checker.fullPeriodicCheck(ctx) assert.NoError(t, err) }) diff --git a/internal/healthcheck/dns/dns.go b/internal/healthcheck/dns/dns.go new file mode 100644 index 00000000..13e7d591 --- /dev/null +++ b/internal/healthcheck/dns/dns.go @@ -0,0 +1,39 @@ +package dns + +import ( + "context" + "errors" + "fmt" + "net" +) + +// Client is a simple plaintext UDP DNS client, to be used for healthchecks. +// Note the client connects to a DNS server only over UDP on port 53, +// because we don't want to use DoT or DoH and impact the TCP connections +// when running a healthcheck. +type Client struct{} + +func New() *Client { + return &Client{} +} + +var ErrLookupNoIPs = errors.New("no IPs found from DNS lookup") + +func (c *Client) Check(ctx context.Context) error { + resolver := &net.Resolver{ + PreferGo: true, + Dial: func(ctx context.Context, _, _ string) (net.Conn, error) { + dialer := net.Dialer{} + return dialer.DialContext(ctx, "udp", "1.1.1.1:53") + }, + } + ips, err := resolver.LookupIP(ctx, "ip", "github.com") + switch { + case err != nil: + return err + case len(ips) == 0: + return fmt.Errorf("%w", ErrLookupNoIPs) + default: + return nil + } +} diff --git a/internal/healthcheck/handler.go b/internal/healthcheck/handler.go index 20b7e888..2b9ee99b 100644 --- a/internal/healthcheck/handler.go +++ b/internal/healthcheck/handler.go @@ -9,13 +9,15 @@ import ( type handler struct { healthErr error healthErrMu sync.RWMutex + logger Logger } var errHealthcheckNotRunYet = errors.New("healthcheck did not run yet") -func newHandler() *handler { +func newHandler(logger Logger) *handler { return &handler{ healthErr: errHealthcheckNotRunYet, + logger: logger, } } diff --git a/internal/healthcheck/health.go b/internal/healthcheck/health.go deleted file mode 100644 index c93b18b2..00000000 --- a/internal/healthcheck/health.go +++ /dev/null @@ -1,122 +0,0 @@ -package healthcheck - -import ( - "context" - "crypto/tls" - "errors" - "fmt" - "net" - "strings" - "time" -) - -func (s *Server) runHealthcheckLoop(ctx context.Context, done chan<- struct{}) { - defer close(done) - - timeoutIndex := 0 - healthcheckTimeouts := []time.Duration{ - 2 * time.Second, - 4 * time.Second, - 6 * time.Second, - 8 * time.Second, - // This can be useful when the connection is under stress - // See https://github.com/qdm12/gluetun/issues/2270 - 10 * time.Second, - } - s.vpn.healthyTimer = time.NewTimer(s.vpn.healthyWait) - - for { - previousErr := s.handler.getErr() - - timeout := healthcheckTimeouts[timeoutIndex] - healthcheckCtx, healthcheckCancel := context.WithTimeout( - ctx, timeout) - err := s.healthCheck(healthcheckCtx) - healthcheckCancel() - - s.handler.setErr(err) - - switch { - case previousErr != nil && err == nil: // First success - s.logger.Info("healthy!") - timeoutIndex = 0 - s.vpn.healthyTimer.Stop() - s.vpn.healthyWait = *s.config.VPN.Initial - case previousErr == nil && err != nil: // First failure - s.logger.Debug("unhealthy: " + err.Error()) - s.vpn.healthyTimer.Stop() - s.vpn.healthyTimer = time.NewTimer(s.vpn.healthyWait) - case previousErr != nil && err != nil: // Nth failure - if timeoutIndex < len(healthcheckTimeouts)-1 { - timeoutIndex++ - } - select { - case <-s.vpn.healthyTimer.C: - timeoutIndex = 0 // retry next with the smallest timeout - s.onUnhealthyVPN(ctx, err.Error()) - default: - } - case previousErr == nil && err == nil: // Nth success - timer := time.NewTimer(s.config.SuccessWait) - select { - case <-ctx.Done(): - return - case <-timer.C: - } - } - } -} - -func (s *Server) healthCheck(ctx context.Context) (err error) { - // TODO use mullvad API if current provider is Mullvad - - address, err := makeAddressToDial(s.config.TargetAddress) - if err != nil { - return err - } - - const dialNetwork = "tcp4" - connection, err := s.dialer.DialContext(ctx, dialNetwork, address) - if err != nil { - return fmt.Errorf("dialing: %w", err) - } - - if strings.HasSuffix(address, ":443") { - host, _, err := net.SplitHostPort(address) - if err != nil { - return fmt.Errorf("splitting host and port: %w", err) - } - tlsConfig := &tls.Config{ - MinVersion: tls.VersionTLS12, - ServerName: host, - } - tlsConnection := tls.Client(connection, tlsConfig) - err = tlsConnection.HandshakeContext(ctx) - if err != nil { - return fmt.Errorf("running TLS handshake: %w", err) - } - } - - err = connection.Close() - if err != nil { - return fmt.Errorf("closing connection: %w", err) - } - - return nil -} - -func makeAddressToDial(address string) (addressToDial string, err error) { - host, port, err := net.SplitHostPort(address) - if err != nil { - addrErr := new(net.AddrError) - ok := errors.As(err, &addrErr) - if !ok || addrErr.Err != "missing port in address" { - return "", fmt.Errorf("splitting host and port from address: %w", err) - } - host = address - const defaultPort = "443" - port = defaultPort - } - address = net.JoinHostPort(host, port) - return address, nil -} diff --git a/internal/healthcheck/icmp/apple_ipv4.go b/internal/healthcheck/icmp/apple_ipv4.go new file mode 100644 index 00000000..7f9c6484 --- /dev/null +++ b/internal/healthcheck/icmp/apple_ipv4.go @@ -0,0 +1,49 @@ +package icmp + +import ( + "net" + "time" + + "golang.org/x/net/ipv4" +) + +var _ net.PacketConn = &ipv4Wrapper{} + +// ipv4Wrapper is a wrapper around ipv4.PacketConn to implement +// the net.PacketConn interface. It's only used for Darwin or iOS. +type ipv4Wrapper struct { + ipv4Conn *ipv4.PacketConn +} + +func ipv4ToNetPacketConn(ipv4 *ipv4.PacketConn) *ipv4Wrapper { + return &ipv4Wrapper{ipv4Conn: ipv4} +} + +func (i *ipv4Wrapper) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + n, _, addr, err = i.ipv4Conn.ReadFrom(p) + return n, addr, err +} + +func (i *ipv4Wrapper) WriteTo(p []byte, addr net.Addr) (n int, err error) { + return i.ipv4Conn.WriteTo(p, nil, addr) +} + +func (i *ipv4Wrapper) Close() error { + return i.ipv4Conn.Close() +} + +func (i *ipv4Wrapper) LocalAddr() net.Addr { + return i.ipv4Conn.LocalAddr() +} + +func (i *ipv4Wrapper) SetDeadline(t time.Time) error { + return i.ipv4Conn.SetDeadline(t) +} + +func (i *ipv4Wrapper) SetReadDeadline(t time.Time) error { + return i.ipv4Conn.SetReadDeadline(t) +} + +func (i *ipv4Wrapper) SetWriteDeadline(t time.Time) error { + return i.ipv4Conn.SetWriteDeadline(t) +} diff --git a/internal/healthcheck/icmp/echo.go b/internal/healthcheck/icmp/echo.go new file mode 100644 index 00000000..608abeff --- /dev/null +++ b/internal/healthcheck/icmp/echo.go @@ -0,0 +1,190 @@ +package icmp + +import ( + "bytes" + "context" + cryptorand "crypto/rand" + "encoding/binary" + "errors" + "fmt" + "io" + "math/rand/v2" + "net" + "net/netip" + "strings" + + "golang.org/x/net/icmp" + "golang.org/x/net/ipv4" + "golang.org/x/net/ipv6" +) + +var ( + ErrICMPBodyUnsupported = errors.New("ICMP body type is not supported") + ErrICMPEchoDataMismatch = errors.New("ICMP data mismatch") +) + +type Echoer struct { + buffer []byte + randomSource io.Reader + logger Logger +} + +func NewEchoer(logger Logger) *Echoer { + const maxICMPEchoSize = 1500 + buffer := make([]byte, maxICMPEchoSize) + var seed [32]byte + _, _ = cryptorand.Read(seed[:]) + randomSource := rand.NewChaCha8(seed) + return &Echoer{ + buffer: buffer, + randomSource: randomSource, + logger: logger, + } +} + +var ( + ErrTimedOut = errors.New("timed out waiting for ICMP echo reply") + ErrNotPermitted = errors.New("not permitted") +) + +func (i *Echoer) Echo(ctx context.Context, ip netip.Addr) (err error) { + var ipVersion string + var conn net.PacketConn + if ip.Is4() { + ipVersion = "v4" + conn, err = listenICMPv4(ctx) + } else { + ipVersion = "v6" + conn, err = listenICMPv6(ctx) + } + if err != nil { + if strings.HasSuffix(err.Error(), "socket: operation not permitted") { + err = fmt.Errorf("%w: you can try adding NET_RAW capability to resolve this", ErrNotPermitted) + } + return fmt.Errorf("listening for ICMP packets: %w", err) + } + + go func() { + <-ctx.Done() + conn.Close() + }() + + const echoDataSize = 32 + id, message := buildMessageToSend(ipVersion, echoDataSize, i.randomSource) + + encodedMessage, err := message.Marshal(nil) + if err != nil { + return fmt.Errorf("encoding ICMP message: %w", err) + } + + _, err = conn.WriteTo(encodedMessage, &net.IPAddr{IP: ip.AsSlice()}) + if err != nil { + if strings.HasSuffix(err.Error(), "sendto: operation not permitted") { + err = fmt.Errorf("%w", ErrNotPermitted) + } + return fmt.Errorf("writing ICMP message: %w", err) + } + + receivedData, err := receiveEchoReply(conn, id, i.buffer, ipVersion, i.logger) + if err != nil { + if errors.Is(err, net.ErrClosed) && ctx.Err() != nil { + return fmt.Errorf("%w", ErrTimedOut) + } + return fmt.Errorf("receiving ICMP echo reply: %w", err) + } + + sentData := message.Body.(*icmp.Echo).Data //nolint:forcetypeassert + if !bytes.Equal(receivedData, sentData) { + return fmt.Errorf("%w: sent %x and received %x", ErrICMPEchoDataMismatch, sentData, receivedData) + } + + return nil +} + +func buildMessageToSend(ipVersion string, size uint, randomSource io.Reader) (id int, message *icmp.Message) { + const uint16Bytes = 2 + idBytes := make([]byte, uint16Bytes) + _, _ = randomSource.Read(idBytes) + id = int(binary.BigEndian.Uint16(idBytes)) + + var icmpType icmp.Type + switch ipVersion { + case "v4": + icmpType = ipv4.ICMPTypeEcho + case "v6": + icmpType = ipv6.ICMPTypeEchoRequest + default: + panic(fmt.Sprintf("IP version %q not supported", ipVersion)) + } + messageBodyData := make([]byte, size) + _, _ = randomSource.Read(messageBodyData) + + // See https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml#icmp-parameters-types + message = &icmp.Message{ + Type: icmpType, // echo request + Code: 0, // no code + Checksum: 0, // calculated at encoding (ipv4) or sending (ipv6) + Body: &icmp.Echo{ + ID: id, + Seq: 0, // only one packet + Data: messageBodyData, + }, + } + return id, message +} + +func receiveEchoReply(conn net.PacketConn, id int, buffer []byte, ipVersion string, logger Logger, +) (data []byte, err error) { + var icmpProtocol int + const ( + icmpv4Protocol = 1 + icmpv6Protocol = 58 + ) + switch ipVersion { + case "v4": + icmpProtocol = icmpv4Protocol + case "v6": + icmpProtocol = icmpv6Protocol + default: + panic(fmt.Sprintf("unknown IP version: %s", ipVersion)) + } + + for { + // Note we need to read the whole packet in one call to ReadFrom, so the buffer + // must be large enough to read the entire reply packet. See: + // https://groups.google.com/g/golang-nuts/c/5dy2Q4nPs08/m/KmuSQAGEtG4J + bytesRead, _, err := conn.ReadFrom(buffer) + if err != nil { + return nil, fmt.Errorf("reading from ICMP connection: %w", err) + } + packetBytes := buffer[:bytesRead] + + // Parse the ICMP message + message, err := icmp.ParseMessage(icmpProtocol, packetBytes) + if err != nil { + return nil, fmt.Errorf("parsing message: %w", err) + } + + switch body := message.Body.(type) { + case *icmp.Echo: + if id != body.ID { + logger.Warnf("ignoring ICMP echo reply mismatching expected id %d (id: %d, type: %d, code: %d, length: %d)", + id, body.ID, message.Type, message.Code, len(packetBytes)) + continue // not the ID we are looking for + } + return body.Data, nil + case *icmp.DstUnreach: + logger.Debugf("ignoring ICMP destination unreachable message (type: 3, code: %d, expected-id %d)", message.Code, id) + // See https://github.com/qdm12/gluetun/pull/2923#issuecomment-3377532249 + // on why we ignore this message. If it is actually unreachable, the timeout on waiting for + // the echo reply will do instead of returning an error error. + continue + case *icmp.TimeExceeded: + logger.Debugf("ignoring ICMP time exceeded message (type: 11, code: %d, expected-id %d)", message.Code, id) + continue + default: + return nil, fmt.Errorf("%w: %T (type %d, code %d, expected-id %d)", + ErrICMPBodyUnsupported, body, message.Type, message.Code, id) + } + } +} diff --git a/internal/healthcheck/icmp/interfaces.go b/internal/healthcheck/icmp/interfaces.go new file mode 100644 index 00000000..62979247 --- /dev/null +++ b/internal/healthcheck/icmp/interfaces.go @@ -0,0 +1,6 @@ +package icmp + +type Logger interface { + Debugf(format string, args ...any) + Warnf(format string, args ...any) +} diff --git a/internal/healthcheck/icmp/listen.go b/internal/healthcheck/icmp/listen.go new file mode 100644 index 00000000..7c01c12c --- /dev/null +++ b/internal/healthcheck/icmp/listen.go @@ -0,0 +1,35 @@ +package icmp + +import ( + "context" + "fmt" + "net" + "runtime" + + "golang.org/x/net/ipv4" +) + +func listenICMPv4(ctx context.Context) (conn net.PacketConn, err error) { + var listenConfig net.ListenConfig + const listenAddress = "" + packetConn, err := listenConfig.ListenPacket(ctx, "ip4:icmp", listenAddress) + if err != nil { + return nil, fmt.Errorf("listening for ICMP packets: %w", err) + } + + if runtime.GOOS == "darwin" || runtime.GOOS == "ios" { + packetConn = ipv4ToNetPacketConn(ipv4.NewPacketConn(packetConn)) + } + + return packetConn, nil +} + +func listenICMPv6(ctx context.Context) (conn net.PacketConn, err error) { + var listenConfig net.ListenConfig + const listenAddress = "" + packetConn, err := listenConfig.ListenPacket(ctx, "ip6:ipv6-icmp", listenAddress) + if err != nil { + return nil, fmt.Errorf("listening for ICMPv6 packets: %w", err) + } + return packetConn, nil +} diff --git a/internal/healthcheck/logger.go b/internal/healthcheck/interfaces.go similarity index 52% rename from internal/healthcheck/logger.go rename to internal/healthcheck/interfaces.go index 52a7a546..87c349e6 100644 --- a/internal/healthcheck/logger.go +++ b/internal/healthcheck/interfaces.go @@ -1,7 +1,8 @@ package healthcheck type Logger interface { - Debug(s string) + Debugf(format string, args ...any) Info(s string) + Warnf(format string, args ...any) Error(s string) } diff --git a/internal/healthcheck/openvpn.go b/internal/healthcheck/openvpn.go deleted file mode 100644 index 6d82491f..00000000 --- a/internal/healthcheck/openvpn.go +++ /dev/null @@ -1,25 +0,0 @@ -package healthcheck - -import ( - "context" - "time" - - "github.com/qdm12/gluetun/internal/constants" -) - -type vpnHealth struct { - loop StatusApplier - healthyWait time.Duration - healthyTimer *time.Timer -} - -func (s *Server) onUnhealthyVPN(ctx context.Context, lastErrMessage string) { - s.logger.Info("program has been unhealthy for " + - s.vpn.healthyWait.String() + ": restarting VPN (healthcheck error: " + lastErrMessage + ")") - s.logger.Info("👉 See https://github.com/qdm12/gluetun-wiki/blob/main/faq/healthcheck.md") - s.logger.Info("DO NOT OPEN AN ISSUE UNLESS YOU HAVE READ AND TRIED EVERY POSSIBLE SOLUTION") - _, _ = s.vpn.loop.ApplyStatus(ctx, constants.Stopped) - _, _ = s.vpn.loop.ApplyStatus(ctx, constants.Running) - s.vpn.healthyWait += *s.config.VPN.Addition - s.vpn.healthyTimer = time.NewTimer(s.vpn.healthyWait) -} diff --git a/internal/healthcheck/run.go b/internal/healthcheck/run.go index 5f7bb7fc..3d092514 100644 --- a/internal/healthcheck/run.go +++ b/internal/healthcheck/run.go @@ -10,9 +10,6 @@ import ( func (s *Server) Run(ctx context.Context, done chan<- struct{}) { defer close(done) - loopDone := make(chan struct{}) - go s.runHealthcheckLoop(ctx, loopDone) - server := http.Server{ Addr: s.config.ServerAddress, Handler: s.handler, @@ -37,6 +34,5 @@ func (s *Server) Run(ctx context.Context, done chan<- struct{}) { s.logger.Error(err.Error()) } - <-loopDone <-serverDone } diff --git a/internal/healthcheck/server.go b/internal/healthcheck/server.go index c3a3a6be..4360ad69 100644 --- a/internal/healthcheck/server.go +++ b/internal/healthcheck/server.go @@ -2,7 +2,6 @@ package healthcheck import ( "context" - "net" "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/models" @@ -11,30 +10,21 @@ import ( type Server struct { logger Logger handler *handler - dialer *net.Dialer config settings.Health - vpn vpnHealth } -func NewServer(config settings.Health, - logger Logger, vpnLoop StatusApplier, -) *Server { +func NewServer(config settings.Health, logger Logger) *Server { return &Server{ logger: logger, - handler: newHandler(), - dialer: &net.Dialer{ - Resolver: &net.Resolver{ - PreferGo: true, - }, - }, - config: config, - vpn: vpnHealth{ - loop: vpnLoop, - healthyWait: *config.VPN.Initial, - }, + handler: newHandler(logger), + config: config, } } +func (s *Server) SetError(err error) { + s.handler.setErr(err) +} + type StatusApplier interface { ApplyStatus(ctx context.Context, status models.LoopStatus) ( outcome string, err error) diff --git a/internal/provider/expressvpn/updater/hardcoded.go b/internal/provider/expressvpn/updater/hardcoded.go index f3b8cdab..89beb202 100644 --- a/internal/provider/expressvpn/updater/hardcoded.go +++ b/internal/provider/expressvpn/updater/hardcoded.go @@ -4,7 +4,6 @@ import ( "github.com/qdm12/gluetun/internal/models" ) -//nolint:lll func hardcodedServers() (servers []models.Server) { return []models.Server{ {Country: "Albania", Hostname: "albania-ca-version-2.expressnetw.com"}, @@ -12,69 +11,83 @@ func hardcodedServers() (servers []models.Server) { {Country: "Andorra", Hostname: "andorra-ca-version-2.expressnetw.com"}, {Country: "Argentina", Hostname: "argentina-ca-version-2.expressnetw.com"}, {Country: "Armenia", Hostname: "armenia-ca-version-2.expressnetw.com"}, + {Country: "Australia", City: "Adelaide", Hostname: "australia-adelaide--ca-version-2.expressnetw.com"}, {Country: "Australia", City: "Brisbane", Hostname: "australia-brisbane-ca-version-2.expressnetw.com"}, {Country: "Australia", City: "Melbourne", Hostname: "australia-melbourne-ca-version-2.expressnetw.com"}, {Country: "Australia", City: "Perth", Hostname: "australia-perth-ca-version-2.expressnetw.com"}, {Country: "Australia", City: "Sydney", Hostname: "australia-sydney-2-ca-version-2.expressnetw.com"}, {Country: "Australia", City: "Sydney", Hostname: "australia-sydney-ca-version-2.expressnetw.com"}, + {Country: "Australia", City: "Woolloomooloo", Hostname: "australia-woolloomooloo-2-ca-version-2.expressnetw.com"}, {Country: "Austria", Hostname: "austria-ca-version-2.expressnetw.com"}, + {Country: "Azerbaijan", Hostname: "azerbaijan-ca-version-2.expressnetw.com"}, {Country: "Bahamas", Hostname: "bahamas-ca-version-2.expressnetw.com"}, {Country: "Bangladesh", Hostname: "bangladesh-ca-version-2.expressnetw.com"}, {Country: "Belarus", Hostname: "belarus-ca-version-2.expressnetw.com"}, {Country: "Belgium", Hostname: "belgium-ca-version-2.expressnetw.com"}, + {Country: "Bermuda", Hostname: "bermuda-ca-version-2.expressnetw.com"}, {Country: "Bhutan", Hostname: "bhutan-ca-version-2.expressnetw.com"}, - {Country: "Bosnia And Herzegovina", City: "Bosnia And Herzegovina", Hostname: "bosniaandherzegovina-ca-version-2.expressnetw.com"}, + {Country: "Bolivia", Hostname: "bolivia-ca-version-2.expressnetw.com"}, + {Country: "Bosnia and Herzegovina", Hostname: "bosniaandherzegovina-ca-version-2.expressnetw.com"}, {Country: "Brazil", Hostname: "brazil-2-ca-version-2.expressnetw.com"}, {Country: "Brazil", Hostname: "brazil-ca-version-2.expressnetw.com"}, {Country: "Brunei", Hostname: "brunei-ca-version-2.expressnetw.com"}, + {Country: "Bulgaria", Hostname: "bulgaria-ca-version-2.expressnetw.com"}, {Country: "Cambodia", Hostname: "cambodia-ca-version-2.expressnetw.com"}, {Country: "Canada", City: "Montreal", Hostname: "canada-montreal-ca-version-2.expressnetw.com"}, - {Country: "Canada", City: "Montreal", Hostname: "canada-montreal-ca-version-2.expressnetw.com"}, {Country: "Canada", City: "Toronto", Hostname: "canada-toronto-2-ca-version-2.expressnetw.com"}, {Country: "Canada", City: "Toronto", Hostname: "canada-toronto-ca-version-2.expressnetw.com"}, - {Country: "Canada", City: "Vancouver", Hostname: "canada-vancouver-ca-version-2.expressnetw.com"}, + {Country: "Cayman Islands", Hostname: "caymanislands-ca-version-2.expressnetw.com"}, {Country: "Chile", Hostname: "chile-ca-version-2.expressnetw.com"}, {Country: "Colombia", Hostname: "colombia-ca-version-2.expressnetw.com"}, - {Country: "Costa Rica", City: "Costa Rica", Hostname: "costarica-ca-version-2.expressnetw.com"}, + {Country: "Costa Rica", Hostname: "costarica-ca-version-2.expressnetw.com"}, {Country: "Croatia", Hostname: "croatia-ca-version-2.expressnetw.com"}, + {Country: "Cuba", Hostname: "cuba-ca-version-2.expressnetw.com"}, {Country: "Cyprus", Hostname: "cyprus-ca-version-2.expressnetw.com"}, - {Country: "Czech Republic", City: "Czech Republic", Hostname: "czechrepublic-ca-version-2.expressnetw.com"}, + {Country: "Czech Republic", Hostname: "czechrepublic-ca-version-2.expressnetw.com"}, {Country: "Denmark", Hostname: "denmark-ca-version-2.expressnetw.com"}, + {Country: "Dominican Republic", Hostname: "dominicanrepublic-ca-version-2.expressnetw.com"}, {Country: "Ecuador", Hostname: "ecuador-ca-version-2.expressnetw.com"}, {Country: "Egypt", Hostname: "egypt-ca-version-2.expressnetw.com"}, {Country: "Estonia", Hostname: "estonia-ca-version-2.expressnetw.com"}, {Country: "Finland", Hostname: "finland-ca-version-2.expressnetw.com"}, + {Country: "France", City: "Alsace", Hostname: "france-alsace-ca-version-2.expressnetw.com"}, + {Country: "France", City: "Marseille", Hostname: "france-marseille-ca-version-2.expressnetw.com"}, {Country: "France", City: "Paris", Hostname: "france-paris-1-ca-version-2.expressnetw.com"}, {Country: "France", City: "Paris", Hostname: "france-paris-2-ca-version-2.expressnetw.com"}, {Country: "France", City: "Strasbourg", Hostname: "france-strasbourg-ca-version-2.expressnetw.com"}, {Country: "Georgia", Hostname: "georgia-ca-version-2.expressnetw.com"}, - {Country: "Germany", City: "Frankfurt", Hostname: "germany-frankfurt-1-ca-version-2.expressnetw.com"}, - {Country: "Germany", City: "Frankfurt", Hostname: "germany-frankfurt-2-ca-version-2.expressnetw.com"}, {Country: "Germany", City: "Frankfurt", Hostname: "germany-darmstadt-ca-version-2.expressnetw.com"}, + {Country: "Germany", City: "Frankfurt", Hostname: "germany-frankfurt-1-ca-version-2.expressnetw.com"}, {Country: "Germany", City: "Nuremberg", Hostname: "germany-nuremberg-ca-version-2.expressnetw.com"}, + {Country: "Ghana", Hostname: "ghana-ca-version-2.expressnetw.com"}, {Country: "Greece", Hostname: "greece-ca-version-2.expressnetw.com"}, + {Country: "Guam", Hostname: "guam-ca-version-2.expressnetw.com"}, {Country: "Guatemala", Hostname: "guatemala-ca-version-2.expressnetw.com"}, - {Country: "Hong Kong", City: "Hong Kong", Hostname: "hongkong-2-ca-version-2.expressnetw.com"}, - {Country: "Hong Kong", City: "Hong Kong", Hostname: "hongkong4-ca-version-2.expressnetw.com"}, + {Country: "Honduras", Hostname: "honduras-ca-version-2.expressnetw.com"}, + {Country: "Hong Kong", Hostname: "hongkong-1-ca-version-2.expressnetw.com"}, + {Country: "Hong Kong", Hostname: "hongkong-2-ca-version-2.expressnetw.com"}, {Country: "Hungary", Hostname: "hungary-ca-version-2.expressnetw.com"}, {Country: "Iceland", Hostname: "iceland-ca-version-2.expressnetw.com"}, - {Country: "India", City: "Chennai", Hostname: "india-chennai-ca-version-2.expressnetw.com"}, - {Country: "India", City: "Mumbai", Hostname: "india-mumbai-1-ca-version-2.expressnetw.com"}, + {Country: "India (via Singapore)", Hostname: "india-sg-ca-version-2.expressnetw.com"}, + {Country: "India (via UK)", Hostname: "india-uk-ca-version-2.expressnetw.com"}, {Country: "Indonesia", Hostname: "indonesia-ca-version-2.expressnetw.com"}, {Country: "Ireland", Hostname: "ireland-ca-version-2.expressnetw.com"}, - {Country: "Isle Of Man", City: "Isle Of Man", Hostname: "isleofman-ca-version-2.expressnetw.com"}, + {Country: "Isle of Man", Hostname: "isleofman-ca-version-2.expressnetw.com"}, {Country: "Israel", Hostname: "israel-ca-version-2.expressnetw.com"}, {Country: "Italy", City: "Cosenza", Hostname: "italy-cosenza-ca-version-2.expressnetw.com"}, {Country: "Italy", City: "Milan", Hostname: "italy-milan-ca-version-2.expressnetw.com"}, - {Country: "Japan", City: "Kawasaki", Hostname: "japan-kawasaki-ca-version-2.expressnetw.com"}, - {Country: "Japan", City: "Tokyo", Hostname: "japan-tokyo-1-ca-version-2.expressnetw.com"}, - {Country: "Japan", City: "Tokyo", Hostname: "japan-tokyo-2-ca-version-2.expressnetw.com"}, + {Country: "Italy", City: "Naples", Hostname: "italy-naples-ca-version-2.expressnetw.com"}, + {Country: "Jamaica", Hostname: "jamaica-ca-version-2.expressnetw.com"}, + {Country: "Japan", City: "Osaka", Hostname: "japan-osaka-ca-version-2.expressnetw.com"}, + {Country: "Japan", City: "Shibuya", Hostname: "japan-shibuya-ca-version-2.expressnetw.com"}, + {Country: "Japan", City: "Tokyo", Hostname: "japan-tokyo-ca-version-2.expressnetw.com"}, + {Country: "Japan", City: "Yokohama", Hostname: "japan-yokohama-ca-version-2.expressnetw.com"}, {Country: "Jersey", Hostname: "jersey-ca-version-2.expressnetw.com"}, {Country: "Kazakhstan", Hostname: "kazakhstan-ca-version-2.expressnetw.com"}, {Country: "Kenya", Hostname: "kenya-ca-version-2.expressnetw.com"}, - {Country: "Kyrgyzstan", Hostname: "kyrgyzstan-ca-version-2.expressnetw.com"}, {Country: "Laos", Hostname: "laos-ca-version-2.expressnetw.com"}, {Country: "Latvia", Hostname: "latvia-ca-version-2.expressnetw.com"}, + {Country: "Lebanon", Hostname: "lebanon-ca-version-2.expressnetw.com"}, {Country: "Liechtenstein", Hostname: "liechtenstein-ca-version-2.expressnetw.com"}, {Country: "Lithuania", Hostname: "lithuania-ca-version-2.expressnetw.com"}, {Country: "Luxembourg", Hostname: "luxembourg-ca-version-2.expressnetw.com"}, @@ -86,21 +99,22 @@ func hardcodedServers() (servers []models.Server) { {Country: "Monaco", Hostname: "monaco-ca-version-2.expressnetw.com"}, {Country: "Mongolia", Hostname: "mongolia-ca-version-2.expressnetw.com"}, {Country: "Montenegro", Hostname: "montenegro-ca-version-2.expressnetw.com"}, + {Country: "Morocco", Hostname: "morocco-ca-version-2.expressnetw.com"}, {Country: "Myanmar", Hostname: "myanmar-ca-version-2.expressnetw.com"}, {Country: "Nepal", Hostname: "nepal-ca-version-2.expressnetw.com"}, - {Country: "Netherlands", City: "Amsterdam", Hostname: "netherlands-amsterdam-2-ca-version-2.expressnetw.com"}, {Country: "Netherlands", City: "Amsterdam", Hostname: "netherlands-amsterdam-ca-version-2.expressnetw.com"}, {Country: "Netherlands", City: "Rotterdam", Hostname: "netherlands-rotterdam-ca-version-2.expressnetw.com"}, {Country: "Netherlands", City: "The Hague", Hostname: "netherlands-thehague-ca-version-2.expressnetw.com"}, - {Country: "New Zealand", City: "New Zealand", Hostname: "newzealand-ca-version-2.expressnetw.com"}, - {Country: "North Macedonia", City: "North Macedonia", Hostname: "macedonia-ca-version-2.expressnetw.com"}, + {Country: "New Zealand", Hostname: "newzealand-ca-version-2.expressnetw.com"}, + {Country: "North Macedonia", Hostname: "macedonia-ca-version-2.expressnetw.com"}, {Country: "Norway", Hostname: "norway-ca-version-2.expressnetw.com"}, {Country: "Pakistan", Hostname: "pakistan-ca-version-2.expressnetw.com"}, {Country: "Panama", Hostname: "panama-ca-version-2.expressnetw.com"}, {Country: "Peru", Hostname: "peru-ca-version-2.expressnetw.com"}, - {Country: "Philippines Via Singapore", City: "Philippines Via Singapore", Hostname: "ph-via-sing-ca-version-2.expressnetw.com"}, + {Country: "Philippines (via Singapore)", Hostname: "ph-via-sing-ca-version-2.expressnetw.com"}, {Country: "Poland", Hostname: "poland-ca-version-2.expressnetw.com"}, {Country: "Portugal", Hostname: "portugal-ca-version-2.expressnetw.com"}, + {Country: "Puerto Rico", Hostname: "puertorico-ca-version-2.expressnetw.com"}, {Country: "Romania", Hostname: "romania-ca-version-2.expressnetw.com"}, {Country: "Serbia", Hostname: "serbia-ca-version-2.expressnetw.com"}, {Country: "Singapore", City: "CBD", Hostname: "singapore-cbd-ca-version-2.expressnetw.com"}, @@ -108,43 +122,58 @@ func hardcodedServers() (servers []models.Server) { {Country: "Singapore", City: "Marina Bay", Hostname: "singapore-marinabay-ca-version-2.expressnetw.com"}, {Country: "Slovakia", Hostname: "slovakia-ca-version-2.expressnetw.com"}, {Country: "Slovenia", Hostname: "slovenia-ca-version-2.expressnetw.com"}, - {Country: "South Africa", City: "South Africa", Hostname: "southafrica-ca-version-2.expressnetw.com"}, - {Country: "South Korea", City: "South Korea", Hostname: "southkorea2-ca-version-2.expressnetw.com"}, + {Country: "South Africa", Hostname: "southafrica-ca-version-2.expressnetw.com"}, + {Country: "South Korea", Hostname: "southkorea2-ca-version-2.expressnetw.com"}, {Country: "Spain", City: "Barcelona", Hostname: "spain-barcelona-ca-version-2.expressnetw.com"}, + {Country: "Spain", City: "Barcelona", Hostname: "spain-barcelona2-ca-version-2.expressnetw.com"}, {Country: "Spain", City: "Madrid", Hostname: "spain-ca-version-2.expressnetw.com"}, - {Country: "Sri Lanka", City: "Sri Lanka", Hostname: "srilanka-ca-version-2.expressnetw.com"}, + {Country: "Sri Lanka", Hostname: "srilanka-ca-version-2.expressnetw.com"}, {Country: "Sweden", Hostname: "sweden-ca-version-2.expressnetw.com"}, + {Country: "Sweden", Hostname: "sweden2-ca-version-2.expressnetw.com"}, {Country: "Switzerland", Hostname: "switzerland-2-ca-version-2.expressnetw.com"}, {Country: "Switzerland", Hostname: "switzerland-ca-version-2.expressnetw.com"}, - {Country: "Taiwan", Hostname: "taiwan-2-ca-version-2.expressnetw.com"}, + {Country: "Taiwan", Hostname: "taiwan-3-ca-version-2.expressnetw.com"}, {Country: "Thailand", Hostname: "thailand-ca-version-2.expressnetw.com"}, + {Country: "Trinidad and Tobago", Hostname: "trinidadandtobago-ca-version-2.expressnetw.com"}, {Country: "Turkey", Hostname: "turkey-ca-version-2.expressnetw.com"}, - {Country: "Ukraine", Hostname: "ukraine-ca-version-2.expressnetw.com"}, - {Country: "UK", City: "Docklands", Hostname: "uk-berkshire-2-ca-version-2.expressnetw.com"}, - {Country: "UK", City: "London", Hostname: "uk-east-london-ca-version-2.expressnetw.com"}, + {Country: "UK", City: "Docklands", Hostname: "uk-1-docklands-ca-version-2.expressnetw.com"}, + {Country: "UK", City: "East London", Hostname: "uk-east-london-ca-version-2.expressnetw.com"}, {Country: "UK", City: "London", Hostname: "uk-london-ca-version-2.expressnetw.com"}, + {Country: "UK", City: "Midlands", Hostname: "uk-midlands-ca-version-2.expressnetw.com"}, + {Country: "UK", City: "Tottenham", Hostname: "uk-tottenham-ca-version-2.expressnetw.com"}, + {Country: "UK", City: "Wembley", Hostname: "uk-wembley-ca-version-2.expressnetw.com"}, + {Country: "Ukraine", Hostname: "ukraine-ca-version-2.expressnetw.com"}, {Country: "Uruguay", Hostname: "uruguay-ca-version-2.expressnetw.com"}, + {Country: "USA", City: "Albuquerque", Hostname: "usa-albuquerque-ca-version-2.expressnetw.com"}, {Country: "USA", City: "Atlanta", Hostname: "usa-atlanta-ca-version-2.expressnetw.com"}, + {Country: "USA", City: "Boston", Hostname: "us-boston-ca-version-2.expressnetw.com"}, {Country: "USA", City: "Chicago", Hostname: "usa-chicago-ca-version-2.expressnetw.com"}, - {Country: "USA", City: "Dallas", Hostname: "usa-dallas-2-ca-version-2.expressnetw.com"}, {Country: "USA", City: "Dallas", Hostname: "usa-dallas-ca-version-2.expressnetw.com"}, {Country: "USA", City: "Denver", Hostname: "usa-denver-ca-version-2.expressnetw.com"}, - {Country: "USA", City: "Los Angeles", Hostname: "usa-losangeles-1-ca-version-2.expressnetw.com"}, + {Country: "USA", City: "Houston", Hostname: "usa-houston-ca-version-2.expressnetw.com"}, + {Country: "USA", City: "Jackson", Hostname: "us-jackson-ca-version-2.expressnetw.com"}, + {Country: "USA", City: "Lincoln Park", Hostname: "usa-lincolnpark-ca-version-2.expressnetw.com"}, + {Country: "USA", City: "Little Rock", Hostname: "us-littlerock-ca-version-2.expressnetw.com"}, {Country: "USA", City: "Los Angeles", Hostname: "usa-losangeles-2-ca-version-2.expressnetw.com"}, {Country: "USA", City: "Los Angeles", Hostname: "usa-losangeles-3-ca-version-2.expressnetw.com"}, - {Country: "USA", City: "Los Angeles", Hostname: "usa-losangeles5-ca-version-2.expressnetw.com"}, {Country: "USA", City: "Los Angeles", Hostname: "usa-losangeles-ca-version-2.expressnetw.com"}, + {Country: "USA", City: "Los Angeles", Hostname: "usa-losangeles5-ca-version-2.expressnetw.com"}, + {Country: "USA", City: "Miami", Hostname: "usa-miami-2-ca-version-2.expressnetw.com"}, {Country: "USA", City: "Miami", Hostname: "usa-miami-ca-version-2.expressnetw.com"}, {Country: "USA", City: "New Jersey", Hostname: "usa-newjersey-1-ca-version-2.expressnetw.com"}, - {Country: "USA", City: "New Jersey", Hostname: "usa-newjersey2-ca-version-2.expressnetw.com"}, {Country: "USA", City: "New Jersey", Hostname: "usa-newjersey-3-ca-version-2.expressnetw.com"}, - {Country: "USA", City: "New York", Hostname: "us-new-york-2-ca-version-2.expressnetw.com"}, + {Country: "USA", City: "New Jersey", Hostname: "usa-newjersey2-ca-version-2.expressnetw.com"}, + {Country: "USA", City: "New Orleans", Hostname: "us-neworleans-ca-version-2.expressnetw.com"}, {Country: "USA", City: "New York", Hostname: "usa-newyork-ca-version-2.expressnetw.com"}, + {Country: "USA", City: "Oklahoma City", Hostname: "us-oklahoma-ca-version-2.expressnetw.com"}, + {Country: "USA", City: "Phoenix", Hostname: "usa-phoenix-ca-version-2.expressnetw.com"}, {Country: "USA", City: "Salt Lake City", Hostname: "usa-saltlakecity-ca-version-2.expressnetw.com"}, {Country: "USA", City: "San Francisco", Hostname: "usa-sanfrancisco-ca-version-2.expressnetw.com"}, + {Country: "USA", City: "Santa Monica", Hostname: "usa-santa-monica-ca-version-2.expressnetw.com"}, {Country: "USA", City: "Seattle", Hostname: "usa-seattle-ca-version-2.expressnetw.com"}, {Country: "USA", City: "Tampa", Hostname: "usa-tampa-1-ca-version-2.expressnetw.com"}, {Country: "USA", City: "Washington DC", Hostname: "usa-washingtondc-ca-version-2.expressnetw.com"}, + {Country: "USA", City: "Wichita", Hostname: "us-wichita-ca-version-2.expressnetw.com"}, {Country: "Uzbekistan", Hostname: "uzbekistan-ca-version-2.expressnetw.com"}, {Country: "Venezuela", Hostname: "venezuela-ca-version-2.expressnetw.com"}, {Country: "Vietnam", Hostname: "vietnam-ca-version-2.expressnetw.com"}, diff --git a/internal/storage/servers.json b/internal/storage/servers.json index e59c19f7..f7c06f98 100644 --- a/internal/storage/servers.json +++ b/internal/storage/servers.json @@ -22338,7 +22338,7 @@ }, "expressvpn": { "version": 2, - "timestamp": 1658167237, + "timestamp": 1757789493, "servers": [ { "vpn": "openvpn", @@ -22346,7 +22346,8 @@ "hostname": "albania-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "31.171.152.38" + "31.171.152.205", + "31.171.153.178" ] }, { @@ -22355,8 +22356,8 @@ "hostname": "algeria-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "85.203.22.33", - "85.203.22.39" + "45.130.203.199", + "45.130.203.238" ] }, { @@ -22365,7 +22366,8 @@ "hostname": "andorra-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "85.203.22.21" + "85.203.22.21", + "85.203.22.27" ] }, { @@ -22374,8 +22376,8 @@ "hostname": "argentina-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "45.91.48.18", - "45.91.48.28" + "185.64.78.62", + "185.64.78.159" ] }, { @@ -22384,9 +22386,21 @@ "hostname": "armenia-ca-version-2.expressnetw.com", "udp": true, "ips": [ + "85.203.22.52", "85.203.22.56" ] }, + { + "vpn": "openvpn", + "country": "Australia", + "city": "Adelaide", + "hostname": "australia-adelaide--ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "91.124.88.148", + "91.124.88.219" + ] + }, { "vpn": "openvpn", "country": "Australia", @@ -22394,8 +22408,9 @@ "hostname": "australia-brisbane-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "45.132.224.79", - "45.132.224.84" + "85.237.90.124", + "85.237.90.161", + "85.237.90.172" ] }, { @@ -22405,8 +22420,11 @@ "hostname": "australia-melbourne-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "45.132.225.3", - "45.132.225.8" + "140.99.0.5", + "140.99.0.30", + "140.99.0.98", + "140.99.0.141", + "140.99.0.154" ] }, { @@ -22416,8 +22434,8 @@ "hostname": "australia-perth-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "45.133.6.4", - "45.133.6.9" + "45.133.6.169", + "45.133.6.176" ] }, { @@ -22427,8 +22445,9 @@ "hostname": "australia-sydney-2-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "181.214.175.87", - "181.214.175.134" + "146.103.39.163", + "146.103.39.224", + "146.103.39.250" ] }, { @@ -22438,8 +22457,20 @@ "hostname": "australia-sydney-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "45.133.5.163", - "45.133.5.227" + "45.133.5.174", + "45.133.5.226" + ] + }, + { + "vpn": "openvpn", + "country": "Australia", + "city": "Woolloomooloo", + "hostname": "australia-woolloomooloo-2-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "185.255.124.57", + "185.255.124.117", + "185.255.124.193" ] }, { @@ -22448,7 +22479,18 @@ "hostname": "austria-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "45.95.243.205" + "45.95.243.205", + "45.95.243.214" + ] + }, + { + "vpn": "openvpn", + "country": "Azerbaijan", + "hostname": "azerbaijan-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "94.20.222.179", + "94.20.222.195" ] }, { @@ -22457,7 +22499,8 @@ "hostname": "bahamas-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "154.6.23.21" + "64.64.117.29", + "64.64.117.32" ] }, { @@ -22466,7 +22509,8 @@ "hostname": "bangladesh-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "85.203.21.218" + "84.17.39.44", + "84.17.39.79" ] }, { @@ -22475,8 +22519,9 @@ "hostname": "belarus-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "85.203.22.64", - "85.203.22.68" + "85.203.22.68", + "85.203.22.76", + "85.203.22.79" ] }, { @@ -22485,7 +22530,19 @@ "hostname": "belgium-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "194.110.115.116" + "185.132.187.167", + "185.132.187.223", + "185.132.187.227" + ] + }, + { + "vpn": "openvpn", + "country": "Bermuda", + "hostname": "bermuda-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "173.244.43.35", + "173.244.43.77" ] }, { @@ -22494,17 +22551,27 @@ "hostname": "bhutan-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "193.37.32.107" + "84.17.39.70", + "84.17.39.75" ] }, { "vpn": "openvpn", - "country": "Bosnia And Herzegovina", - "city": "Bosnia And Herzegovina", + "country": "Bolivia", + "hostname": "bolivia-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "45.91.48.85", + "45.91.48.87" + ] + }, + { + "vpn": "openvpn", + "country": "Bosnia and Herzegovina", "hostname": "bosniaandherzegovina-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "85.203.22.98", + "85.203.22.101", "85.203.22.105" ] }, @@ -22514,8 +22581,9 @@ "hostname": "brazil-2-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "191.101.252.94", - "191.101.252.116" + "203.188.168.31", + "203.188.168.49", + "203.188.168.141" ] }, { @@ -22524,11 +22592,9 @@ "hostname": "brazil-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "45.91.48.43", - "45.91.48.48", - "45.91.48.53", - "45.91.48.58", - "45.91.48.63" + "2.57.171.14", + "2.57.171.17", + "2.57.171.35" ] }, { @@ -22537,7 +22603,18 @@ "hostname": "brunei-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "85.203.21.58" + "23.27.18.56", + "23.27.18.145" + ] + }, + { + "vpn": "openvpn", + "country": "Bulgaria", + "hostname": "bulgaria-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "66.56.85.162", + "66.56.85.171" ] }, { @@ -22546,7 +22623,8 @@ "hostname": "cambodia-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "85.203.21.54" + "23.27.18.21", + "23.27.18.67" ] }, { @@ -22556,19 +22634,9 @@ "hostname": "canada-montreal-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "107.152.206.69", - "144.168.163.5" - ] - }, - { - "vpn": "openvpn", - "country": "Canada", - "city": "Montreal", - "hostname": "canada-montreal-ca-version-2.expressnetw.com", - "udp": true, - "ips": [ - "107.152.206.69", - "144.168.163.5" + "45.91.23.165", + "45.91.23.195", + "45.91.23.201" ] }, { @@ -22578,8 +22646,9 @@ "hostname": "canada-toronto-2-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "198.144.145.4", - "198.144.145.10" + "89.251.0.217", + "89.251.0.251", + "89.251.0.254" ] }, { @@ -22589,8 +22658,18 @@ "hostname": "canada-toronto-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "50.118.143.41", - "50.118.143.131" + "50.118.143.96", + "50.118.143.120" + ] + }, + { + "vpn": "openvpn", + "country": "Cayman Islands", + "hostname": "caymanislands-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "98.159.39.167", + "98.159.39.206" ] }, { @@ -22599,7 +22678,8 @@ "hostname": "chile-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "45.91.48.78" + "2.57.171.175", + "45.91.48.73" ] }, { @@ -22608,18 +22688,18 @@ "hostname": "colombia-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "45.91.49.20", - "45.91.49.25" + "31.59.107.77", + "31.59.107.209" ] }, { "vpn": "openvpn", "country": "Costa Rica", - "city": "Costa Rica", "hostname": "costarica-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "179.48.251.106" + "203.188.175.225", + "203.188.175.227" ] }, { @@ -22628,8 +22708,18 @@ "hostname": "croatia-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "85.10.56.237", - "85.10.56.244" + "85.203.20.119", + "85.203.20.129" + ] + }, + { + "vpn": "openvpn", + "country": "Cuba", + "hostname": "cuba-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "83.219.96.170", + "83.219.96.198" ] }, { @@ -22638,18 +22728,18 @@ "hostname": "cyprus-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "185.205.186.3", - "213.169.148.152" + "203.188.190.96", + "203.188.190.218" ] }, { "vpn": "openvpn", "country": "Czech Republic", - "city": "Czech Republic", "hostname": "czechrepublic-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "82.118.30.150" + "82.118.30.152", + "203.26.81.85" ] }, { @@ -22658,8 +22748,18 @@ "hostname": "denmark-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "193.29.107.11", - "193.29.107.19" + "91.198.200.72", + "91.198.200.219" + ] + }, + { + "vpn": "openvpn", + "country": "Dominican Republic", + "hostname": "dominicanrepublic-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "31.217.248.45", + "31.217.248.79" ] }, { @@ -22668,6 +22768,7 @@ "hostname": "ecuador-ca-version-2.expressnetw.com", "udp": true, "ips": [ + "45.91.49.35", "45.91.49.40" ] }, @@ -22677,7 +22778,8 @@ "hostname": "egypt-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "85.203.22.113" + "45.130.203.120", + "45.130.203.123" ] }, { @@ -22686,8 +22788,8 @@ "hostname": "estonia-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "185.195.237.147", - "185.195.237.154" + "37.77.56.149", + "37.77.56.157" ] }, { @@ -22696,7 +22798,33 @@ "hostname": "finland-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "196.244.192.2" + "196.244.192.2", + "196.244.192.14" + ] + }, + { + "vpn": "openvpn", + "country": "France", + "city": "Alsace", + "hostname": "france-alsace-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "185.255.126.97", + "185.255.126.223" + ] + }, + { + "vpn": "openvpn", + "country": "France", + "city": "Marseille", + "hostname": "france-marseille-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "45.137.124.101", + "45.137.124.171", + "45.137.124.186", + "45.137.124.205", + "45.137.124.217" ] }, { @@ -22706,11 +22834,8 @@ "hostname": "france-paris-1-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "45.91.22.2", - "45.91.22.7", - "45.91.22.24", - "45.91.22.29", - "45.91.22.50" + "45.91.22.12", + "45.91.22.154" ] }, { @@ -22720,8 +22845,9 @@ "hostname": "france-paris-2-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "193.68.91.77", - "193.68.91.173" + "193.68.91.3", + "193.68.91.16", + "193.68.91.78" ] }, { @@ -22731,10 +22857,8 @@ "hostname": "france-strasbourg-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "191.96.180.110", - "191.96.180.194", - "191.96.180.204", - "191.96.180.222" + "185.194.178.214", + "185.194.178.232" ] }, { @@ -22743,7 +22867,8 @@ "hostname": "georgia-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "91.239.206.88" + "91.239.206.59", + "91.239.206.69" ] }, { @@ -22753,7 +22878,8 @@ "hostname": "germany-darmstadt-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "84.247.59.250" + "85.203.15.185", + "85.203.15.223" ] }, { @@ -22763,8 +22889,8 @@ "hostname": "germany-frankfurt-1-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "193.68.92.193", - "193.68.92.195" + "193.68.92.146", + "193.68.92.183" ] }, { @@ -22775,7 +22901,17 @@ "udp": true, "ips": [ "212.30.36.109", - "212.30.36.131" + "212.30.36.136" + ] + }, + { + "vpn": "openvpn", + "country": "Ghana", + "hostname": "ghana-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "5.180.179.52", + "5.180.179.74" ] }, { @@ -22784,8 +22920,18 @@ "hostname": "greece-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "185.51.134.218", - "185.51.134.226" + "89.213.226.75", + "89.213.226.80" + ] + }, + { + "vpn": "openvpn", + "country": "Guam", + "hostname": "guam-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "45.129.85.40", + "45.129.85.148" ] }, { @@ -22794,18 +22940,39 @@ "hostname": "guatemala-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "45.91.49.50" + "2.57.171.244", + "45.91.49.45" + ] + }, + { + "vpn": "openvpn", + "country": "Honduras", + "hostname": "honduras-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "31.217.248.148", + "31.217.248.220" + ] + }, + { + "vpn": "openvpn", + "country": "Hong Kong", + "hostname": "hongkong-1-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "194.5.83.123", + "194.5.83.158" ] }, { "vpn": "openvpn", "country": "Hong Kong", - "city": "Hong Kong", "hostname": "hongkong-2-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "193.176.211.87", - "193.176.211.117" + "193.176.211.91", + "193.176.211.119", + "193.176.211.131" ] }, { @@ -22814,10 +22981,8 @@ "hostname": "hungary-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "91.219.236.214", - "91.219.237.148", - "91.219.238.100", - "91.219.239.222" + "155.2.217.53", + "155.2.217.97" ] }, { @@ -22826,10 +22991,29 @@ "hostname": "iceland-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "45.133.193.142", - "45.133.193.147", - "45.133.193.152", - "45.133.193.155" + "45.86.201.5", + "45.86.201.6", + "45.86.201.107" + ] + }, + { + "vpn": "openvpn", + "country": "India (via Singapore)", + "hostname": "india-sg-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "194.61.40.200", + "194.61.40.213" + ] + }, + { + "vpn": "openvpn", + "country": "India (via UK)", + "hostname": "india-uk-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "45.137.126.147", + "45.137.126.148" ] }, { @@ -22838,7 +23022,8 @@ "hostname": "indonesia-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "64.64.121.6" + "45.8.25.112", + "45.8.25.124" ] }, { @@ -22847,24 +23032,20 @@ "hostname": "ireland-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "37.120.235.19", - "37.120.235.36", - "77.81.139.115", - "185.192.16.190", - "188.241.178.132", - "188.241.178.140", - "188.241.178.156", - "188.241.178.172" + "185.192.16.226", + "185.192.16.232", + "185.192.16.240", + "185.192.16.250" ] }, { "vpn": "openvpn", - "country": "Isle Of Man", - "city": "Isle Of Man", + "country": "Isle of Man", "hostname": "isleofman-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "85.203.22.129" + "85.203.22.137", + "85.203.22.145" ] }, { @@ -22873,9 +23054,8 @@ "hostname": "israel-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "31.168.162.18", - "82.81.85.236", - "185.191.204.147" + "203.159.81.117", + "203.159.81.192" ] }, { @@ -22885,11 +23065,9 @@ "hostname": "italy-cosenza-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "213.21.226.22", - "213.21.226.42", - "213.21.226.90", + "213.21.226.43", "213.21.226.94", - "213.21.226.142" + "213.21.226.198" ] }, { @@ -22899,43 +23077,78 @@ "hostname": "italy-milan-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "45.91.20.143" + "45.91.20.143", + "45.91.20.193" + ] + }, + { + "vpn": "openvpn", + "country": "Italy", + "city": "Naples", + "hostname": "italy-naples-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "141.11.36.135", + "141.11.36.209" + ] + }, + { + "vpn": "openvpn", + "country": "Jamaica", + "hostname": "jamaica-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "83.219.96.60", + "83.219.96.70" ] }, { "vpn": "openvpn", "country": "Japan", - "city": "Kawasaki", - "hostname": "japan-kawasaki-ca-version-2.expressnetw.com", + "city": "Osaka", + "hostname": "japan-osaka-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "194.5.48.19", - "194.5.48.47", - "194.5.48.84", - "194.5.48.108", - "194.5.48.130" + "203.25.124.62", + "203.25.124.154", + "203.25.124.195", + "203.25.124.217", + "203.25.124.231" + ] + }, + { + "vpn": "openvpn", + "country": "Japan", + "city": "Shibuya", + "hostname": "japan-shibuya-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "45.84.219.53", + "45.84.219.118" ] }, { "vpn": "openvpn", "country": "Japan", "city": "Tokyo", - "hostname": "japan-tokyo-1-ca-version-2.expressnetw.com", + "hostname": "japan-tokyo-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "185.208.11.10", - "185.208.11.151" + "185.208.11.64", + "185.208.11.94", + "185.208.11.117" ] }, { "vpn": "openvpn", "country": "Japan", - "city": "Tokyo", - "hostname": "japan-tokyo-2-ca-version-2.expressnetw.com", + "city": "Yokohama", + "hostname": "japan-yokohama-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "45.8.68.16", - "45.8.68.27" + "213.21.247.15", + "213.21.247.37", + "213.21.247.241" ] }, { @@ -22944,6 +23157,7 @@ "hostname": "jersey-ca-version-2.expressnetw.com", "udp": true, "ips": [ + "85.203.22.163", "85.203.22.166" ] }, @@ -22953,7 +23167,8 @@ "hostname": "kazakhstan-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "84.17.39.5" + "84.17.39.5", + "84.17.39.7" ] }, { @@ -22962,16 +23177,8 @@ "hostname": "kenya-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "62.12.114.54" - ] - }, - { - "vpn": "openvpn", - "country": "Kyrgyzstan", - "hostname": "kyrgyzstan-ca-version-2.expressnetw.com", - "udp": true, - "ips": [ - "91.213.233.57" + "203.188.189.161", + "203.188.189.234" ] }, { @@ -22980,7 +23187,8 @@ "hostname": "laos-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "185.233.133.8" + "23.27.18.41", + "185.233.133.82" ] }, { @@ -22989,7 +23197,18 @@ "hostname": "latvia-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "196.196.52.3" + "196.196.52.3", + "196.196.106.68" + ] + }, + { + "vpn": "openvpn", + "country": "Lebanon", + "hostname": "lebanon-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "5.180.179.228", + "5.180.179.238" ] }, { @@ -22998,6 +23217,7 @@ "hostname": "liechtenstein-ca-version-2.expressnetw.com", "udp": true, "ips": [ + "85.203.22.179", "85.203.22.180" ] }, @@ -23007,7 +23227,8 @@ "hostname": "lithuania-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "85.206.166.86" + "85.206.169.8", + "85.206.169.151" ] }, { @@ -23016,6 +23237,7 @@ "hostname": "luxembourg-ca-version-2.expressnetw.com", "udp": true, "ips": [ + "185.221.132.205", "185.221.132.212", "185.221.132.220" ] @@ -23026,7 +23248,8 @@ "hostname": "macau-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "85.203.21.134" + "23.27.18.170", + "185.233.133.22" ] }, { @@ -23035,7 +23258,8 @@ "hostname": "malaysia-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "5.182.16.237" + "173.239.236.78", + "173.239.236.196" ] }, { @@ -23044,7 +23268,8 @@ "hostname": "malta-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "85.203.22.9" + "85.203.22.7", + "85.203.22.8" ] }, { @@ -23053,7 +23278,8 @@ "hostname": "mexico-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "179.61.245.10" + "190.93.96.211", + "190.93.96.225" ] }, { @@ -23062,7 +23288,8 @@ "hostname": "moldova-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "178.17.168.130" + "178.175.129.12", + "178.175.129.20" ] }, { @@ -23071,7 +23298,8 @@ "hostname": "monaco-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "85.203.22.210" + "85.203.22.209", + "85.203.22.212" ] }, { @@ -23080,7 +23308,8 @@ "hostname": "mongolia-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "185.233.133.61" + "23.27.18.62", + "23.27.18.197" ] }, { @@ -23089,16 +23318,28 @@ "hostname": "montenegro-ca-version-2.expressnetw.com", "udp": true, "ips": [ + "85.203.22.229", "85.203.22.233" ] }, + { + "vpn": "openvpn", + "country": "Morocco", + "hostname": "morocco-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "185.137.164.79", + "185.137.164.167" + ] + }, { "vpn": "openvpn", "country": "Myanmar", "hostname": "myanmar-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "85.203.21.199" + "23.27.18.149", + "23.27.18.150" ] }, { @@ -23107,7 +23348,8 @@ "hostname": "nepal-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "193.37.32.82" + "23.27.18.194", + "23.27.18.198" ] }, { @@ -23117,8 +23359,8 @@ "hostname": "netherlands-amsterdam-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "45.128.199.193", - "45.128.199.200" + "173.239.199.138", + "173.239.199.253" ] }, { @@ -23128,7 +23370,8 @@ "hostname": "netherlands-rotterdam-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "212.30.37.246" + "212.30.37.194", + "212.30.37.233" ] }, { @@ -23138,29 +23381,29 @@ "hostname": "netherlands-thehague-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "193.68.95.206", - "193.68.95.212" + "193.68.95.15", + "193.68.95.129" ] }, { "vpn": "openvpn", "country": "New Zealand", - "city": "New Zealand", "hostname": "newzealand-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "45.133.7.21", - "45.133.7.38" + "85.237.91.217", + "85.237.91.242", + "85.237.91.250" ] }, { "vpn": "openvpn", "country": "North Macedonia", - "city": "North Macedonia", "hostname": "macedonia-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "85.203.22.193" + "85.203.22.193", + "85.203.22.202" ] }, { @@ -23169,8 +23412,8 @@ "hostname": "norway-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "45.92.228.159", - "45.92.228.249" + "45.13.191.60", + "45.13.191.136" ] }, { @@ -23179,7 +23422,8 @@ "hostname": "pakistan-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "85.203.21.225" + "23.27.18.89", + "23.27.18.185" ] }, { @@ -23188,7 +23432,8 @@ "hostname": "panama-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "45.91.49.55" + "45.91.49.55", + "45.91.49.60" ] }, { @@ -23197,21 +23442,18 @@ "hostname": "peru-ca-version-2.expressnetw.com", "udp": true, "ips": [ + "45.91.49.65", "45.91.49.70" ] }, { "vpn": "openvpn", - "country": "Philippines Via Singapore", - "city": "Philippines Via Singapore", + "country": "Philippines (via Singapore)", "hostname": "ph-via-sing-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "84.17.38.71", - "84.17.38.232", - "84.17.39.103", - "138.199.25.144", - "138.199.25.214" + "194.61.41.142", + "194.61.41.174" ] }, { @@ -23220,8 +23462,9 @@ "hostname": "poland-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "188.212.135.78", - "188.212.135.239" + "188.212.135.8", + "188.212.135.25", + "188.212.135.216" ] }, { @@ -23230,10 +23473,18 @@ "hostname": "portugal-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "94.46.13.79", - "94.46.167.4", - "94.46.167.74", - "94.46.167.214" + "185.228.3.121", + "185.228.3.206" + ] + }, + { + "vpn": "openvpn", + "country": "Puerto Rico", + "hostname": "puertorico-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "98.159.39.56", + "98.159.39.94" ] }, { @@ -23242,8 +23493,8 @@ "hostname": "romania-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "185.195.19.197", - "185.195.19.203" + "185.195.19.196", + "185.195.19.200" ] }, { @@ -23252,7 +23503,8 @@ "hostname": "serbia-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "89.38.224.163" + "89.38.224.154", + "89.38.224.162" ] }, { @@ -23262,7 +23514,8 @@ "hostname": "singapore-cbd-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "194.5.82.112" + "194.5.82.174", + "194.5.82.224" ] }, { @@ -23272,8 +23525,8 @@ "hostname": "singapore-jurong-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "185.233.133.54", - "185.233.133.211" + "185.233.133.52", + "185.233.133.193" ] }, { @@ -23283,8 +23536,10 @@ "hostname": "singapore-marinabay-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "45.80.156.183", - "45.85.107.86" + "45.80.6.45", + "45.80.6.232", + "45.80.6.235", + "45.80.6.239" ] }, { @@ -23293,7 +23548,8 @@ "hostname": "slovakia-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "193.37.255.220" + "66.56.86.49", + "66.56.86.59" ] }, { @@ -23302,30 +23558,28 @@ "hostname": "slovenia-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "85.203.22.133" + "85.203.22.133", + "85.203.22.143" ] }, { "vpn": "openvpn", "country": "South Africa", - "city": "South Africa", "hostname": "southafrica-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "154.16.95.10", - "154.16.95.27", - "154.16.95.39" + "185.132.186.177", + "185.132.186.202" ] }, { "vpn": "openvpn", "country": "South Korea", - "city": "South Korea", "hostname": "southkorea2-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "185.92.24.40", - "185.92.24.235" + "185.92.24.28", + "185.92.24.189" ] }, { @@ -23335,11 +23589,19 @@ "hostname": "spain-barcelona-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "45.130.136.18", - "45.130.136.39", - "45.130.136.54", - "45.130.136.133", - "45.130.136.153" + "45.130.136.248", + "185.253.99.92" + ] + }, + { + "vpn": "openvpn", + "country": "Spain", + "city": "Barcelona", + "hostname": "spain-barcelona2-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "45.67.99.235", + "45.67.99.243" ] }, { @@ -23349,18 +23611,20 @@ "hostname": "spain-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "212.30.33.143", - "212.30.33.213" + "89.38.70.39", + "89.38.70.105", + "89.38.70.144", + "89.38.70.153" ] }, { "vpn": "openvpn", "country": "Sri Lanka", - "city": "Sri Lanka", "hostname": "srilanka-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "85.203.21.203" + "185.233.133.48", + "185.233.133.70" ] }, { @@ -23369,9 +23633,18 @@ "hostname": "sweden-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "45.135.187.121", - "45.135.187.205", - "45.135.187.215" + "45.135.187.110", + "45.135.187.196" + ] + }, + { + "vpn": "openvpn", + "country": "Sweden", + "hostname": "sweden2-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "178.212.224.123", + "178.212.224.240" ] }, { @@ -23380,8 +23653,9 @@ "hostname": "switzerland-2-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "85.203.45.2", - "85.203.45.8" + "85.203.45.28", + "85.203.45.34", + "85.203.45.245" ] }, { @@ -23390,16 +23664,18 @@ "hostname": "switzerland-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "45.132.226.145" + "45.132.226.116", + "45.132.226.118" ] }, { "vpn": "openvpn", "country": "Taiwan", - "hostname": "taiwan-2-ca-version-2.expressnetw.com", + "hostname": "taiwan-3-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "194.5.52.84" + "45.133.176.181", + "45.133.176.201" ] }, { @@ -23408,7 +23684,18 @@ "hostname": "thailand-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "194.5.49.72" + "98.159.43.187", + "98.159.43.219" + ] + }, + { + "vpn": "openvpn", + "country": "Trinidad and Tobago", + "hostname": "trinidadandtobago-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "173.244.43.153", + "173.244.43.204" ] }, { @@ -23417,29 +23704,30 @@ "hostname": "turkey-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "45.8.24.214" + "45.130.202.132", + "45.130.202.188" ] }, { "vpn": "openvpn", "country": "UK", "city": "Docklands", - "hostname": "uk-berkshire-2-ca-version-2.expressnetw.com", + "hostname": "uk-1-docklands-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "185.208.9.191", - "185.208.9.244" + "185.208.9.17", + "185.208.9.117" ] }, { "vpn": "openvpn", "country": "UK", - "city": "London", + "city": "East London", "hostname": "uk-east-london-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "45.84.216.74", - "45.84.216.183" + "45.84.216.123", + "45.84.216.147" ] }, { @@ -23449,8 +23737,53 @@ "hostname": "uk-london-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "5.157.128.8", - "5.157.128.24" + "5.157.128.101" + ] + }, + { + "vpn": "openvpn", + "country": "UK", + "city": "Midlands", + "hostname": "uk-midlands-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "212.2.238.246", + "213.21.231.115" + ] + }, + { + "vpn": "openvpn", + "country": "UK", + "city": "Tottenham", + "hostname": "uk-tottenham-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "31.171.130.173", + "31.171.130.215", + "31.171.130.247" + ] + }, + { + "vpn": "openvpn", + "country": "UK", + "city": "Wembley", + "hostname": "uk-wembley-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "185.199.157.123", + "185.199.157.210", + "185.199.157.222" + ] + }, + { + "vpn": "openvpn", + "country": "USA", + "city": "Albuquerque", + "hostname": "usa-albuquerque-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "98.159.233.129", + "98.159.233.178" ] }, { @@ -23461,7 +23794,18 @@ "udp": true, "ips": [ "45.80.157.75", - "45.80.157.170" + "45.80.157.190" + ] + }, + { + "vpn": "openvpn", + "country": "USA", + "city": "Boston", + "hostname": "us-boston-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "151.240.45.179", + "151.240.45.237" ] }, { @@ -23471,18 +23815,10 @@ "hostname": "usa-chicago-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "149.19.196.116", - "149.19.196.239" - ] - }, - { - "vpn": "openvpn", - "country": "USA", - "city": "Dallas", - "hostname": "usa-dallas-2-ca-version-2.expressnetw.com", - "udp": true, - "ips": [ - "23.230.233.137" + "149.19.196.24", + "149.19.196.98", + "149.19.196.159", + "149.19.196.228" ] }, { @@ -23492,8 +23828,8 @@ "hostname": "usa-dallas-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "45.80.159.87", - "45.80.159.232" + "45.8.206.10", + "45.80.159.34" ] }, { @@ -23503,8 +23839,53 @@ "hostname": "usa-denver-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "185.238.231.95", - "185.238.231.100" + "89.116.76.162", + "89.116.76.250" + ] + }, + { + "vpn": "openvpn", + "country": "USA", + "city": "Houston", + "hostname": "usa-houston-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "142.111.152.213", + "142.111.152.217", + "142.111.152.245" + ] + }, + { + "vpn": "openvpn", + "country": "USA", + "city": "Jackson", + "hostname": "us-jackson-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "193.36.220.61", + "193.36.220.219" + ] + }, + { + "vpn": "openvpn", + "country": "USA", + "city": "Lincoln Park", + "hostname": "usa-lincolnpark-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "154.16.59.49", + "154.16.59.138" + ] + }, + { + "vpn": "openvpn", + "country": "USA", + "city": "Little Rock", + "hostname": "us-littlerock-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "195.210.106.68", + "195.210.106.94" ] }, { @@ -23514,7 +23895,8 @@ "hostname": "usa-losangeles-2-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "23.230.125.38" + "23.230.125.27", + "23.230.125.130" ] }, { @@ -23524,8 +23906,8 @@ "hostname": "usa-losangeles-3-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "45.84.212.22", - "45.84.212.142" + "45.84.212.142", + "45.84.212.209" ] }, { @@ -23535,8 +23917,9 @@ "hostname": "usa-losangeles-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "45.38.57.63", - "45.38.57.134" + "45.38.57.26", + "45.38.57.89", + "45.38.57.201" ] }, { @@ -23546,8 +23929,22 @@ "hostname": "usa-losangeles5-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "45.81.75.153", - "45.85.89.167" + "104.234.227.131", + "104.234.227.198" + ] + }, + { + "vpn": "openvpn", + "country": "USA", + "city": "Miami", + "hostname": "usa-miami-2-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "45.84.213.45", + "45.84.213.102", + "45.84.213.134", + "45.84.213.150", + "45.84.213.203" ] }, { @@ -23557,8 +23954,8 @@ "hostname": "usa-miami-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "191.101.177.213", - "191.101.177.225" + "104.234.149.86", + "104.234.149.129" ] }, { @@ -23568,7 +23965,8 @@ "hostname": "usa-newjersey-1-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "45.38.140.13" + "45.38.140.72", + "45.38.140.147" ] }, { @@ -23578,8 +23976,8 @@ "hostname": "usa-newjersey-3-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "45.85.91.41", - "45.85.91.71" + "45.85.91.71", + "45.85.91.208" ] }, { @@ -23589,8 +23987,20 @@ "hostname": "usa-newjersey2-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "191.101.174.183", - "191.101.174.209" + "192.253.209.60", + "192.253.209.193", + "192.253.209.245" + ] + }, + { + "vpn": "openvpn", + "country": "USA", + "city": "New Orleans", + "hostname": "us-neworleans-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "195.210.127.32", + "195.210.127.98" ] }, { @@ -23600,10 +24010,32 @@ "hostname": "usa-newyork-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "173.239.207.44", - "173.239.207.130", - "173.239.207.161", - "173.239.207.174" + "173.239.207.174", + "173.239.207.176", + "216.177.135.133", + "216.177.135.154" + ] + }, + { + "vpn": "openvpn", + "country": "USA", + "city": "Oklahoma City", + "hostname": "us-oklahoma-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "46.173.253.81", + "46.173.253.118" + ] + }, + { + "vpn": "openvpn", + "country": "USA", + "city": "Phoenix", + "hostname": "usa-phoenix-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "216.24.212.90", + "216.24.212.118" ] }, { @@ -23613,8 +24045,8 @@ "hostname": "usa-saltlakecity-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "191.96.206.244", - "191.96.206.248" + "89.116.182.136", + "89.116.182.182" ] }, { @@ -23624,8 +24056,20 @@ "hostname": "usa-sanfrancisco-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "45.38.178.25", - "173.239.198.33" + "45.38.178.138", + "45.38.178.163" + ] + }, + { + "vpn": "openvpn", + "country": "USA", + "city": "Santa Monica", + "hostname": "usa-santa-monica-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "104.234.225.41", + "104.234.225.102", + "213.254.160.100" ] }, { @@ -23635,7 +24079,8 @@ "hostname": "usa-seattle-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "50.118.162.1" + "50.118.162.4", + "50.118.162.174" ] }, { @@ -23645,8 +24090,9 @@ "hostname": "usa-tampa-1-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "191.96.170.163", - "191.96.170.186" + "188.213.202.217", + "188.213.202.232", + "188.213.202.233" ] }, { @@ -23656,9 +24102,20 @@ "hostname": "usa-washingtondc-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "45.39.207.159", - "45.39.207.201", - "45.39.207.250" + "45.39.207.52", + "45.39.207.107", + "45.39.207.205" + ] + }, + { + "vpn": "openvpn", + "country": "USA", + "city": "Wichita", + "hostname": "us-wichita-ca-version-2.expressnetw.com", + "udp": true, + "ips": [ + "195.210.125.79", + "195.210.125.133" ] }, { @@ -23667,8 +24124,8 @@ "hostname": "ukraine-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "45.130.81.55", - "45.130.81.63" + "45.130.81.52", + "45.130.81.64" ] }, { @@ -23677,7 +24134,8 @@ "hostname": "uruguay-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "45.91.49.75" + "45.91.49.75", + "45.91.49.80" ] }, { @@ -23686,7 +24144,7 @@ "hostname": "uzbekistan-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "85.203.22.80", + "85.203.22.84", "85.203.22.89" ] }, @@ -23696,7 +24154,8 @@ "hostname": "venezuela-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "45.91.49.85" + "45.91.49.85", + "45.91.49.90" ] }, { @@ -23705,7 +24164,8 @@ "hostname": "vietnam-ca-version-2.expressnetw.com", "udp": true, "ips": [ - "85.203.21.39" + "104.164.168.37", + "104.164.168.140" ] } ] diff --git a/internal/vpn/interfaces.go b/internal/vpn/interfaces.go index fa075bbd..b24e0753 100644 --- a/internal/vpn/interfaces.go +++ b/internal/vpn/interfaces.go @@ -100,3 +100,13 @@ type CmdStarter interface { stdoutLines, stderrLines <-chan string, waitError <-chan error, startErr error) } + +type HealthChecker interface { + SetConfig(tlsDialAddr string, icmpTarget netip.Addr) + Start(ctx context.Context) (runError <-chan error, err error) + Stop() error +} + +type HealthServer interface { + SetError(err error) +} diff --git a/internal/vpn/loop.go b/internal/vpn/loop.go index 40bbf488..c83ad11a 100644 --- a/internal/vpn/loop.go +++ b/internal/vpn/loop.go @@ -13,10 +13,13 @@ import ( ) type Loop struct { - statusManager *loopstate.State - state *state.State - providers Providers - storage Storage + statusManager *loopstate.State + state *state.State + providers Providers + storage Storage + healthSettings settings.Health + healthChecker HealthChecker + healthServer HealthServer // Fixed parameters buildInfo models.BuildInformation versionInfo bool @@ -49,7 +52,8 @@ const ( ) func NewLoop(vpnSettings settings.VPN, ipv6Supported bool, vpnInputPorts []uint16, - providers Providers, storage Storage, openvpnConf OpenVPN, + providers Providers, storage Storage, healthSettings settings.Health, + healthChecker HealthChecker, healthServer HealthServer, openvpnConf OpenVPN, netLinker NetLinker, fw Firewall, routing Routing, portForward PortForward, starter CmdStarter, publicip PublicIPLoop, dnsLooper DNSLoop, @@ -65,29 +69,32 @@ func NewLoop(vpnSettings settings.VPN, ipv6Supported bool, vpnInputPorts []uint1 state := state.New(statusManager, vpnSettings) return &Loop{ - statusManager: statusManager, - state: state, - providers: providers, - storage: storage, - buildInfo: buildInfo, - versionInfo: versionInfo, - ipv6Supported: ipv6Supported, - vpnInputPorts: vpnInputPorts, - openvpnConf: openvpnConf, - netLinker: netLinker, - fw: fw, - routing: routing, - portForward: portForward, - publicip: publicip, - dnsLooper: dnsLooper, - starter: starter, - logger: logger, - client: client, - start: start, - running: running, - stop: stop, - stopped: stopped, - userTrigger: true, - backoffTime: defaultBackoffTime, + statusManager: statusManager, + state: state, + providers: providers, + storage: storage, + healthSettings: healthSettings, + healthChecker: healthChecker, + healthServer: healthServer, + buildInfo: buildInfo, + versionInfo: versionInfo, + ipv6Supported: ipv6Supported, + vpnInputPorts: vpnInputPorts, + openvpnConf: openvpnConf, + netLinker: netLinker, + fw: fw, + routing: routing, + portForward: portForward, + publicip: publicip, + dnsLooper: dnsLooper, + starter: starter, + logger: logger, + client: client, + start: start, + running: running, + stop: stop, + stopped: stopped, + userTrigger: true, + backoffTime: defaultBackoffTime, } } diff --git a/internal/vpn/openvpn.go b/internal/vpn/openvpn.go index c9842f43..c6e8bc9e 100644 --- a/internal/vpn/openvpn.go +++ b/internal/vpn/openvpn.go @@ -15,8 +15,7 @@ import ( func setupOpenVPN(ctx context.Context, fw Firewall, openvpnConf OpenVPN, providerConf provider.Provider, settings settings.VPN, ipv6Supported bool, starter CmdStarter, - logger openvpn.Logger) (runner *openvpn.Runner, - connection models.Connection, err error, + logger openvpn.Logger) (runner *openvpn.Runner, connection models.Connection, err error, ) { connection, err = providerConf.GetConnection(settings.Provider.ServerSelection, ipv6Supported) if err != nil { diff --git a/internal/vpn/run.go b/internal/vpn/run.go index 971e76c0..e42d6216 100644 --- a/internal/vpn/run.go +++ b/internal/vpn/run.go @@ -48,6 +48,7 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) { } tunnelUpData := tunnelUpData{ vpnType: settings.Type, + serverIP: connection.IP, serverName: connection.ServerName, canPortForward: connection.PortForward, portForwarder: portForwarder, @@ -75,7 +76,7 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) { for stayHere { select { case <-tunnelReady: - go l.onTunnelUp(openvpnCtx, tunnelUpData) + go l.onTunnelUp(openvpnCtx, ctx, tunnelUpData) case <-ctx.Done(): l.cleanup() openvpnCancel() diff --git a/internal/vpn/tunnelup.go b/internal/vpn/tunnelup.go index 4ae32b8e..72ba9006 100644 --- a/internal/vpn/tunnelup.go +++ b/internal/vpn/tunnelup.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "net/netip" "time" "github.com/qdm12/dns/v2/pkg/check" @@ -14,13 +15,13 @@ import ( ) type tunnelUpData struct { - // vpnIntf is the name of the VPN network interface - // which is used both for port forwarding and MTU discovery - vpnIntf string + // Healthcheck + serverIP netip.Addr // vpnType is used for path MTU discovery to find the protocol overhead. // It can be "wireguard" or "openvpn". vpnType string - // Port forwarding fields: + // Port forwarding + vpnIntf string serverName string // used for PIA canPortForward bool // used for PIA username string // used for PIA @@ -28,7 +29,7 @@ type tunnelUpData struct { portForwarder PortForwarder } -func (l *Loop) onTunnelUp(ctx context.Context, data tunnelUpData) { +func (l *Loop) onTunnelUp(ctx, loopCtx context.Context, data tunnelUpData) { l.client.CloseIdleConnections() mtuLogger := l.logger.New(log.SetComponent("MTU discovery")) @@ -45,6 +46,24 @@ func (l *Loop) onTunnelUp(ctx context.Context, data tunnelUpData) { } } + icmpTarget := l.healthSettings.ICMPTargetIP + if icmpTarget.IsUnspecified() { + icmpTarget = data.serverIP + } + l.healthChecker.SetConfig(l.healthSettings.TargetAddress, icmpTarget) + + healthErrCh, err := l.healthChecker.Start(ctx) + l.healthServer.SetError(err) + if err != nil { + // Note this restart call must be done in a separate goroutine + // from the VPN loop goroutine. + l.restartVPN(loopCtx, err) + return + } + defer func() { + _ = l.healthChecker.Stop() + }() + if *l.dnsLooper.GetSettings().DoT.Enabled { _, _ = l.dnsLooper.ApplyStatus(ctx, constants.Running) } else { @@ -73,6 +92,23 @@ func (l *Loop) onTunnelUp(ctx context.Context, data tunnelUpData) { if err != nil { l.logger.Error(err.Error()) } + + select { + case <-ctx.Done(): + case healthErr := <-healthErrCh: + l.healthServer.SetError(healthErr) + // Note this restart call must be done in a separate goroutine + // from the VPN loop goroutine. + l.restartVPN(loopCtx, healthErr) + } +} + +func (l *Loop) restartVPN(ctx context.Context, healthErr error) { + l.logger.Warnf("restarting VPN because it failed to pass the healthcheck: %s", healthErr) + l.logger.Info("👉 See https://github.com/qdm12/gluetun-wiki/blob/main/faq/healthcheck.md") + l.logger.Info("DO NOT OPEN AN ISSUE UNLESS YOU HAVE READ AND TRIED EVERY POSSIBLE SOLUTION") + _, _ = l.ApplyStatus(ctx, constants.Stopped) + _, _ = l.ApplyStatus(ctx, constants.Running) } var errVPNTypeUnknown = errors.New("unknown VPN type")