diff --git a/go.mod b/go.mod
index 40666d63018d7b021e2808bcf05fd5162300199c..4c8836b331cf220f9d828f64b469e40a5211fbd5 100644
--- a/go.mod
+++ b/go.mod
@@ -21,7 +21,7 @@ require (
 )
 
 require (
-	0xacab.org/leap/bitmask-core v0.0.0-20240830161617-121267e4c4eb
+	0xacab.org/leap/bitmask-core v0.0.0-20241015185856-0812b9aadf98
 	github.com/natefinch/npipe v0.0.0-20160621034901-c1b8fa8bdcce
 	github.com/prometheus-community/pro-bing v0.4.0
 	github.com/rs/zerolog v1.33.0
diff --git a/go.sum b/go.sum
index f02a51c3d272a087f369eabed0a5bdf6a0f84a54..14d62c1635e245729a9a770c2b4e84bd78da657f 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,5 @@
-0xacab.org/leap/bitmask-core v0.0.0-20240830161617-121267e4c4eb h1:RDLhdZKH4UAwEg8PCFpgqagsPvsx6s+CVrH2Ty6Wn/8=
-0xacab.org/leap/bitmask-core v0.0.0-20240830161617-121267e4c4eb/go.mod h1:iGWa13tANV0T5ZPgWskx0wHW/FLRsRmG7UOZLixuC7I=
+0xacab.org/leap/bitmask-core v0.0.0-20241015185856-0812b9aadf98 h1:0tsyOms2hcHOe9V1aJ8fhRpw/DxpswZzCisIvulLamk=
+0xacab.org/leap/bitmask-core v0.0.0-20241015185856-0812b9aadf98/go.mod h1:VDzbMRiY/bCOu4z1DwpzzqgQyj6i82TpuvuCU6ogl6Q=
 0xacab.org/leap/obfsvpn v1.1.0 h1:mRiV4jzmQOeRVdhbUQbNdT/aj3rmFqhL5GsK8hIol2E=
 0xacab.org/leap/obfsvpn v1.1.0/go.mod h1:o9Jj+5lBfDO/HjJvO7sjnOjziKtwLnbbxMSXrhwupGA=
 0xacab.org/leap/tunnel-telemetry v0.0.0-20240830081933-7328bb50078b h1:GI6SVhECFVdHalARYd5Qt/i5/+M0fjcLtJIEEQVO4Sk=
@@ -551,6 +551,8 @@ golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1m
 golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
 golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb h1:c0vyKkb6yr3KR7jEfJaOSv4lG7xPkbN6r52aJz1d8a8=
+golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
diff --git a/vendor/0xacab.org/leap/bitmask-core/pkg/bootstrap/init.go b/vendor/0xacab.org/leap/bitmask-core/pkg/bootstrap/init.go
index d4bd129523da664c1962ef9f7f19a4c048029589..68041247bd541c95aafe95c308dd3f81973d1c8b 100644
--- a/vendor/0xacab.org/leap/bitmask-core/pkg/bootstrap/init.go
+++ b/vendor/0xacab.org/leap/bitmask-core/pkg/bootstrap/init.go
@@ -16,12 +16,11 @@ import (
 	"0xacab.org/leap/bitmask-core/pkg/client"
 	"0xacab.org/leap/bitmask-core/pkg/client/provisioning"
 	"0xacab.org/leap/bitmask-core/pkg/introducer"
+	bitmask_storage "0xacab.org/leap/bitmask-core/pkg/storage"
 	"0xacab.org/leap/tunnel-telemetry/pkg/geolocate"
 )
 
 type Config struct {
-	// FallbackCountryCode is an ISO-2 country code. Fallback if geolocation lookup fails.
-	FallbackCountryCode string
 	// country code used to fetch gateways/bridges. If geolocation lookup fails,
 	// api.config.FallbackCountryCode is used
 	CountryCode string
@@ -79,9 +78,10 @@ func NewAPI(cfg *Config) (*API, error) {
 	transportConfig := client.DefaultTransportConfig()
 
 	var intro *introducer.Introducer
+	var err error
 
 	if cfg.Introducer != "" {
-		intro, err := introducer.NewIntroducerFromURL(cfg.Introducer)
+		intro, err = introducer.NewIntroducerFromURL(cfg.Introducer)
 		if err != nil {
 			return nil, err
 		}
@@ -117,13 +117,15 @@ func NewAPI(cfg *Config) (*API, error) {
 	// In the future, we might want to add a timeout and mark it as unusable if it fails.
 	if intro != nil {
 		client, err := introducer.NewHTTPClientFromIntroducer(intro)
-		if client != nil {
+		if err != nil {
 			return nil, err
 		}
-		log.Info().Msg("Using obfuscated http client")
+		log.Info().
+			Str("type", intro.Type).
+			Str("addr", intro.Addr).
+			Bool("UseKCP", intro.KCP).
+			Msg("Using introducer")
 		api.httpClient = client
-		// We got an http client configured to use the obfuscated introducer,
-		// so we'll stop here.
 		return api, nil
 	}
 
@@ -166,28 +168,44 @@ func getSocksProxyClient(proxyString string) (*http.Client, error) {
 // DoGeolocationLookup will try to fetch a valid country code.This country
 // code will be stored and sent in any subsequent resource queries to menshen
 // This method should be called only once, right after initializing the API object.
-// The VPN must be turned off when calling this function.
-func (api *API) DoGeolocationLookup() (string, error) {
+// The VPN must be turned off when calling this function. When the geolocation
+// lookup succeeds, the country code is saved on disk for the future, in case
+// the geolcation lookup fails.
+
+func (api *API) DoGeolocationLookup() error {
 	log.Debug().Msg("Doing geolocataion lookup")
+
+	storage, err := bitmask_storage.GetStorage()
+	if err != nil {
+		return fmt.Errorf("Could not get storage to load/save country code fallback: %s", err)
+	}
+
 	geo, err := geolocate.FindCurrentHostGeolocationWithSTUN(api.config.STUNServers, api.config.CountryCodeLookupURL)
+
 	if err == nil {
-		// FIXME scrub if we're going to submit logs.
-		log.Info().
-			Str("countryCode", geo.CC).
-			Msg("Successfully got country code")
 		api.config.CountryCode = geo.CC
+		storage.SaveFallbackCountryCode(geo.CC)
+		return nil
+	}
+
+	log.Warn().
+		Err(err).
+		Str("stunServers", strings.Join(api.config.STUNServers, ",")).
+		Str("countryCodeLookupURL", api.config.CountryCodeLookupURL).
+		Msg("Could not get country code using STUN servers. " +
+			"Trying to use previously fetched country code")
+
+	cc := storage.GetFallbackCountryCode()
+	if cc == "" {
+		log.Warn().Msg("No fallback country code was saved. Proceeding without country code")
 	} else {
-		if api.config.FallbackCountryCode != "" {
-			log.Warn().
-				Err(err).
-				Str("fallbackCountryCode", api.config.FallbackCountryCode).
-				Msg("Could not get country code via geolocation lookup. Using fallback country code")
-			api.config.CountryCode = api.config.FallbackCountryCode
-		} else {
-			return "", err
-		}
+		log.Info().
+			Str("countryCode", cc).
+			Msg("Using fallback country code")
+		api.config.CountryCode = cc
 	}
-	return api.config.CountryCode, nil
+
+	return nil
 }
 
 func (api *API) GetProvider() (*models.ModelsProvider, error) {
@@ -243,6 +261,14 @@ type GatewayParams struct {
 	CC        string
 }
 
+type BridgeParams struct {
+	Location  string
+	Port      string
+	Transport string
+	CC        string
+	Type      string
+}
+
 // GetGateways returns a list of gateways (it it's enabled by the menshen
 // API). It optionally accepts a GatewayParams object where you can set
 // different filters.
@@ -265,6 +291,24 @@ func (api *API) GetGateways(p *GatewayParams) ([]*models.ModelsGateway, error) {
 	return gateways.Payload, err
 }
 
+func (api *API) GetAllBridges(p *BridgeParams) ([]*models.ModelsBridge, error) {
+	params := provisioning.NewGetAPI5BridgesParams()
+	if p != nil {
+		params.Loc = &p.Location
+		params.Port = &p.Port
+		params.Tr = &p.Transport
+		params.Type = &p.Type
+	}
+	if api.httpClient != nil {
+		params = params.WithHTTPClient(api.httpClient)
+	}
+	bridges, err := api.client.Provisioning.GetAPI5Bridges(params)
+	if err != nil {
+		return nil, err
+	}
+	return bridges.Payload, nil
+}
+
 // GetOpenVPNCert returns valid OpenVPN client credentials (certificate and
 // private key)
 func (api *API) GetOpenVPNCert() (string, error) {
@@ -288,14 +332,9 @@ func (api *API) SerializeConfig(params *GatewayParams) (string, error) {
 		return "", err
 	}
 
-	var key string
-	if strings.Contains(rawCert, rsaBegin) {
-		key = matchDelimitedString(rawCert, rsaBegin, rsaEnd)
-	} else {
-		key = matchDelimitedString(rawCert, keyBegin, keyEnd)
-	}
-
+	key := matchDelimitedString(rawCert, keyBegin, keyEnd)
 	crt := matchDelimitedString(rawCert, certBegin, certEnd)
+	ca := matchDelimitedString(rawCert, caBegin, caEnd)
 	gateways, err := api.GetGateways(params)
 	if err != nil {
 		return "", err
@@ -305,7 +344,7 @@ func (api *API) SerializeConfig(params *GatewayParams) (string, error) {
 	gw := gateways[0]
 
 	vars := configVars{
-		CA:        riseupCA,
+		CA:        ca,
 		Cert:      crt,
 		Key:       key,
 		IPAddr:    gw.IPAddr,
diff --git a/vendor/0xacab.org/leap/bitmask-core/pkg/bootstrap/serialize.go b/vendor/0xacab.org/leap/bitmask-core/pkg/bootstrap/serialize.go
index f11327894c623869f55a656c5a6a1298a33dc805..8a63878dbb5942a29d7167af659cad35a3e94edf 100644
--- a/vendor/0xacab.org/leap/bitmask-core/pkg/bootstrap/serialize.go
+++ b/vendor/0xacab.org/leap/bitmask-core/pkg/bootstrap/serialize.go
@@ -39,12 +39,12 @@ remote {{ .IPAddr }} {{ .Port }} {{ .Transport }}
 </key>`
 
 var (
-	rsaBegin  = "-----BEGIN RSA PRIVATE KEY-----"
-	rsaEnd    = "-----END RSA PRIVATE KEY-----"
-	keyBegin  = "-----BEGIN PRIVATE KEY-----"
-	keyEnd    = "-----END PRIVATE KEY-----"
-	certBegin = "-----BEGIN CERTIFICATE-----"
-	certEnd   = "-----END CERTIFICATE-----"
+	keyBegin  = "<key>\n"
+	keyEnd    = "</key>\n"
+	certBegin = "<cert>\n"
+	certEnd   = "</cert>\n"
+	caBegin   = "<ca>\n"
+	caEnd     = "</ca>\n"
 )
 
 func matchDelimitedString(str, left, right string) string {
@@ -53,49 +53,5 @@ func matchDelimitedString(str, left, right string) string {
 	if len(matches) != 1 {
 		return ""
 	}
-	return left + matches[0][1] + right
+	return matches[0][1]
 }
-
-// TODO get the CA from the service too
-var riseupCA = `-----BEGIN CERTIFICATE-----
-MIIFjTCCA3WgAwIBAgIBATANBgkqhkiG9w0BAQ0FADBZMRgwFgYDVQQKDA9SaXNl
-dXAgTmV0d29ya3MxGzAZBgNVBAsMEmh0dHBzOi8vcmlzZXVwLm5ldDEgMB4GA1UE
-AwwXUmlzZXVwIE5ldHdvcmtzIFJvb3QgQ0EwHhcNMTQwNDI4MDAwMDAwWhcNMjQw
-NDI4MDAwMDAwWjBZMRgwFgYDVQQKDA9SaXNldXAgTmV0d29ya3MxGzAZBgNVBAsM
-Emh0dHBzOi8vcmlzZXVwLm5ldDEgMB4GA1UEAwwXUmlzZXVwIE5ldHdvcmtzIFJv
-b3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC76J4ciMJ8Sg0m
-TP7DF2DT9zNe0Csk4myoMFC57rfJeqsAlJCv1XMzBmXrw8wq/9z7XHv6n/0sWU7a
-7cF2hLR33ktjwODlx7vorU39/lXLndo492ZBhXQtG1INMShyv+nlmzO6GT7ESfNE
-LliFitEzwIegpMqxCIHXFuobGSCWF4N0qLHkq/SYUMoOJ96O3hmPSl1kFDRMtWXY
-iw1SEKjUvpyDJpVs3NGxeLCaA7bAWhDY5s5Yb2fA1o8ICAqhowurowJpW7n5ZuLK
-5VNTlNy6nZpkjt1QycYvNycffyPOFm/Q/RKDlvnorJIrihPkyniV3YY5cGgP+Qkx
-HUOT0uLA6LHtzfiyaOqkXwc4b0ZcQD5Vbf6Prd20Ppt6ei0zazkUPwxld3hgyw58
-m/4UIjG3PInWTNf293GngK2Bnz8Qx9e/6TueMSAn/3JBLem56E0WtmbLVjvko+LF
-PM5xA+m0BmuSJtrD1MUCXMhqYTtiOvgLBlUm5zkNxALzG+cXB28k6XikXt6MRG7q
-hzIPG38zwkooM55yy5i1YfcIi5NjMH6A+t4IJxxwb67MSb6UFOwg5kFokdONZcwj
-shczHdG9gLKSBIvrKa03Nd3W2dF9hMbRu//STcQxOailDBQCnXXfAATj9pYzdY4k
-ha8VCAREGAKTDAex9oXf1yRuktES4QIDAQABo2AwXjAdBgNVHQ4EFgQUC4tdmLVu
-f9hwfK4AGliaet5KkcgwDgYDVR0PAQH/BAQDAgIEMAwGA1UdEwQFMAMBAf8wHwYD
-VR0jBBgwFoAUC4tdmLVuf9hwfK4AGliaet5KkcgwDQYJKoZIhvcNAQENBQADggIB
-AGzL+GRnYu99zFoy0bXJKOGCF5XUXP/3gIXPRDqQf5g7Cu/jYMID9dB3No4Zmf7v
-qHjiSXiS8jx1j/6/Luk6PpFbT7QYm4QLs1f4BlfZOti2KE8r7KRDPIecUsUXW6P/
-3GJAVYH/+7OjA39za9AieM7+H5BELGccGrM5wfl7JeEz8in+V2ZWDzHQO4hMkiTQ
-4ZckuaL201F68YpiItBNnJ9N5nHr1MRiGyApHmLXY/wvlrOpclh95qn+lG6/2jk7
-3AmihLOKYMlPwPakJg4PYczm3icFLgTpjV5sq2md9bRyAg3oPGfAuWHmKj2Ikqch
-Td5CHKGxEEWbGUWEMP0s1A/JHWiCbDigc4Cfxhy56CWG4q0tYtnc2GMw8OAUO6Wf
-Xu5pYKNkzKSEtT/MrNJt44tTZWbKV/Pi/N2Fx36my7TgTUj7g3xcE9eF4JV2H/sg
-tsK3pwE0FEqGnT4qMFbixQmc8bGyuakr23wjMvfO7eZUxBuWYR2SkcP26sozF9PF
-tGhbZHQVGZUTVPyvwahMUEhbPGVerOW0IYpxkm0x/eaWdTc4vPpf/rIlgbAjarnJ
-UN9SaWRlWKSdP4haujnzCoJbM7dU9bjvlGZNyXEekgeT0W2qFeGGp+yyUWw8tNsp
-0BuC1b7uW/bBn/xKm319wXVDvBgZgcktMolak39V7DVO
------END CERTIFICATE-----
------BEGIN CERTIFICATE-----
-MIIBYjCCAQigAwIBAgIBATAKBggqhkjOPQQDAjAXMRUwEwYDVQQDEwxMRUFQIFJv
-b3QgQ0EwHhcNMjExMTAyMTkwNTM3WhcNMjYxMTAyMTkxMDM3WjAXMRUwEwYDVQQD
-EwxMRUFQIFJvb3QgQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQxOXBGu+gf
-pjHzVteGTWL6XnFxtEnKMFpKaJkA/VOHmESzoLsZRQxt88GssxaqC01J17idQiqv
-zgNpedmtvFtyo0UwQzAOBgNVHQ8BAf8EBAMCAqQwEgYDVR0TAQH/BAgwBgEB/wIB
-ATAdBgNVHQ4EFgQUZdoUlJrCIUNFrpffAq+LQjnwEz4wCgYIKoZIzj0EAwIDSAAw
-RQIgfr3w4tnRG+NdI3LsGPlsRktGK20xHTzsB3orB0yC6cICIQCB+/9y8nmSStfN
-VUMUyk2hNd7/kC8nL222TTD7VZUtsg==
------END CERTIFICATE-----`
diff --git a/vendor/0xacab.org/leap/bitmask-core/pkg/introducer/httpClient.go b/vendor/0xacab.org/leap/bitmask-core/pkg/introducer/httpClient.go
index 877d353f686c65410c1cabf0d5939adeeddb6745..167a2a2abe05972eae35616a41a13cb9ccb18dd2 100644
--- a/vendor/0xacab.org/leap/bitmask-core/pkg/introducer/httpClient.go
+++ b/vendor/0xacab.org/leap/bitmask-core/pkg/introducer/httpClient.go
@@ -76,8 +76,17 @@ func NewHTTPClientFromIntroducer(introducer *Introducer) (*http.Client, error) {
 		Transport: &CallbackTransport{
 			OriginalTransport: transport,
 			Callback: func(r *http.Response) {
-				if err := storage.MaybeUpdateLastUsedForIntroducer(introducer.URL()); err != nil {
-					log.Error().Err(err).Msg("cannot update introducer")
+				store, err := storage.GetStorage()
+				if err != nil {
+					log.Warn().
+						Err(err).
+						Msg("Could not load DB to update lastUsed timestamp for introducer")
+					return
+				}
+				if err := store.UpdateLastUsedForIntroducer(introducer.URL()); err != nil {
+					log.Warn().
+						Err(err).
+						Msg("Could not update lastUsed timestamp for introducer")
 				}
 			},
 		},
diff --git a/vendor/0xacab.org/leap/bitmask-core/pkg/introducer/introducer.go b/vendor/0xacab.org/leap/bitmask-core/pkg/introducer/introducer.go
index 1c0a745f748b0c455c307d468a1dffbc123ca4a6..0a244acf09178f36e1b73a5970ea29721a2c6160 100644
--- a/vendor/0xacab.org/leap/bitmask-core/pkg/introducer/introducer.go
+++ b/vendor/0xacab.org/leap/bitmask-core/pkg/introducer/introducer.go
@@ -28,7 +28,7 @@ func (i *Introducer) Validate() error {
 	if len(i.Cert) != 70 {
 		return fmt.Errorf("wrong certificate len = %d", len(i.Cert))
 	}
-	if len(strings.Split(i.FQDN, ".")) < 2 {
+	if i.FQDN != "localhost" && len(strings.Split(i.FQDN, ".")) < 2 {
 		return fmt.Errorf("expected a FQDN, got: %s", i.FQDN)
 	}
 	return nil
diff --git a/vendor/0xacab.org/leap/bitmask-core/pkg/models/models.go b/vendor/0xacab.org/leap/bitmask-core/pkg/models/models.go
index 6a5382d15e5221dd08035117a0168b6c49ccc22f..d7073625fd5c12501f78f1d18a873dfc2af81f5a 100644
--- a/vendor/0xacab.org/leap/bitmask-core/pkg/models/models.go
+++ b/vendor/0xacab.org/leap/bitmask-core/pkg/models/models.go
@@ -9,23 +9,21 @@ import "time"
 
 // Introducer keeps metadata about introducers that the user has added to the Bitmask application. Introducers are expected to be transmitted off-band.
 type Introducer struct {
-	ID   int    `storm:"increment"`
-	Name string `storm:"index,unique"`
+	FQDN string
 	// URL is the canonical URL. It should be stored after validation and writing in the canonical order, since
 	// we will check for uniqueness.
-	URL       string    `storm:"unique"`
-	CreatedAt time.Time `storm:"index"`
+	URL       string
+	CreatedAt time.Time
 	LastUsed  time.Time
 }
 
 // Bridge is a private bridge.
 type Bridge struct {
-	ID       int    `storm:"increment"`
-	Name     string `storm:"index,unique"`
+	Name     string
 	Location string
 	Type     string
 	// Raw is the raw JSON serialization of the bridge. We could also use the menshen model as a nested struct.
-	Raw       string    `storm:"unique"`
-	CreatedAt time.Time `storm:"index"`
+	Raw       string
+	CreatedAt time.Time
 	LastUsed  time.Time
 }
diff --git a/vendor/0xacab.org/leap/bitmask-core/pkg/storage/bridge.go b/vendor/0xacab.org/leap/bitmask-core/pkg/storage/bridge.go
deleted file mode 100644
index a3fb7bad019c79de67276f1366aa4e0fae52a5ae..0000000000000000000000000000000000000000
--- a/vendor/0xacab.org/leap/bitmask-core/pkg/storage/bridge.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package storage
-
-import (
-	"github.com/rs/zerolog/log"
-
-	"0xacab.org/leap/bitmask-core/pkg/models"
-)
-
-func MaybeGetBridgeByName(name string) (models.Bridge, error) {
-	var b models.Bridge
-	var err error
-
-	db, err := NewStorageWithDefaultDir()
-	if err != nil {
-		log.Fatal().Err(err).Msg("cannot open storage")
-	}
-	defer db.Close()
-
-	b, err = db.GetBridgeByName(name)
-	if err != nil {
-		return b, err
-	}
-	return b, nil
-}
diff --git a/vendor/0xacab.org/leap/bitmask-core/pkg/storage/db.go b/vendor/0xacab.org/leap/bitmask-core/pkg/storage/db.go
deleted file mode 100644
index fb0bd8a63e6761ba86ebd9b5708cee51587028e4..0000000000000000000000000000000000000000
--- a/vendor/0xacab.org/leap/bitmask-core/pkg/storage/db.go
+++ /dev/null
@@ -1,224 +0,0 @@
-// storage provides an embeddable database that bitmask uses to persist
-// a series of values.
-// For now, we store the following items in the database:
-// - Introducer metadata.
-// - Private bridges.
-package storage
-
-import (
-	"fmt"
-	"os"
-	"path/filepath"
-	"time"
-
-	"github.com/asdine/storm/v3"
-	homedir "github.com/mitchellh/go-homedir"
-	"github.com/rs/zerolog/log"
-
-	"0xacab.org/leap/bitmask-core/pkg/models"
-)
-
-var AppName = "bitmask"
-
-type Storage struct {
-	db *storm.DB
-}
-
-// NewStorage initializes a new storage with a given path. `NewStorageWithDefaultDir` should be preferred to initialize a storage, since it will try to pick a default path.
-func NewStorage(path string) (*Storage, error) {
-	fp := filepath.Join(path, "bitmask.db")
-	db, err := storm.Open(fp)
-	if err != nil {
-		return nil, err
-	}
-
-	// Initialize buckets and indexes
-	if err := db.Init(&models.Introducer{}); err != nil {
-		return nil, err
-	}
-	if err := db.Init(&models.Bridge{}); err != nil {
-		return nil, err
-	}
-	return &Storage{db: db}, nil
-}
-
-func NewStorageWithDefaultDir() (*Storage, error) {
-	home, err := homedir.Dir()
-	if err != nil {
-		return nil, err
-	}
-
-	var configPath string
-	switch _os := os.Getenv("GOOS"); _os {
-	case "windows":
-		configPath = filepath.Join(os.Getenv("APPDATA"), AppName)
-	default:
-		// This will cover both 'darwin' (macOS) and 'linux'
-		configPath = filepath.Join(home, ".config", AppName)
-	}
-
-	err = os.MkdirAll(configPath, 0700)
-	if err != nil {
-		return nil, err
-	}
-
-	return NewStorage(configPath)
-}
-
-// NewIntroducer creates a new Introducer from the name and URL.
-func (s *Storage) NewIntroducer(name, url string) error {
-	item := &models.Introducer{
-		Name: name,
-		URL:  url,
-	}
-	item.CreatedAt = time.Now()
-	return s.db.Save(item)
-}
-
-// ListIntroducers returns an array of all the introducers
-func (s *Storage) ListIntroducers() ([]models.Introducer, error) {
-	var items []models.Introducer
-	err := s.db.AllByIndex("CreatedAt", &items)
-	return items, err
-}
-
-// GetIntroducerByID will return the Introducer with the given ID, if found, and an error.
-func (s *Storage) GetIntroducerByID(id int) (models.Introducer, error) {
-	var introducer models.Introducer
-	err := s.db.One("ID", id, &introducer)
-	return introducer, err
-}
-
-// GetIntroducerByName will return the Introducer with the given Name, if found, and an error.
-func (s *Storage) GetIntroducerByName(name string) (models.Introducer, error) {
-	var introducer models.Introducer
-	err := s.db.One("Name", name, &introducer)
-	return introducer, err
-}
-
-// DeleteIntroducer accepts a name and an ID. If you want to delete by name, pass 0 as the ID;
-// if you want to delete by ID, pass the empty string as name.
-func (s *Storage) DeleteIntroducer(id int, name string) error {
-	if id == 0 && name == "" {
-		return fmt.Errorf("need to pass id or name")
-	}
-	var introducer models.Introducer
-	var err error
-	switch {
-	case id != 0:
-		introducer, err = s.GetIntroducerByID(id)
-		if err != nil {
-			return err
-		}
-	case name != "":
-		introducer, err = s.GetIntroducerByName(name)
-		if err != nil {
-			return err
-		}
-	default:
-		return fmt.Errorf("unhandled case")
-	}
-	return s.db.DeleteStruct(&introducer)
-}
-
-// TODO GetIntroducersNeverUsed - useful for prune
-
-// NewBridge creates a new Bridge from the passed parameters.
-func (s *Storage) NewBridge(name, bridgeType, location, raw string) error {
-	item := &models.Bridge{
-		Name:     name,
-		Type:     bridgeType,
-		Location: location,
-		Raw:      raw,
-	}
-	item.CreatedAt = time.Now()
-	return s.db.Save(item)
-}
-
-// ListBridges returns an array of all the bridges.
-func (s *Storage) ListBridges() ([]models.Bridge, error) {
-	var items []models.Bridge
-	err := s.db.AllByIndex("CreatedAt", &items)
-	return items, err
-}
-
-// GetBridgeByID will return the Bridge with the given ID, if found, and an error.
-func (s *Storage) GetBridgeByID(id int) (models.Bridge, error) {
-	var bridge models.Bridge
-	err := s.db.One("ID", id, &bridge)
-	return bridge, err
-}
-
-// GetBridgeByName will return the Bridge with the given Name, if found, and an error.
-func (s *Storage) GetBridgeByName(name string) (models.Bridge, error) {
-	var bridge models.Bridge
-	err := s.db.One("Name", name, &bridge)
-	return bridge, err
-}
-
-// GetBridgesByType will return all Bridges with the given Type, if found, and an error.
-func (s *Storage) GetBridgesByType(bridgeType string) ([]models.Bridge, error) {
-	var bridges []models.Bridge
-	err := s.db.Find("Type", bridgeType, &bridges)
-	return bridges, err
-}
-
-// GetBridgesByLocation will return all Bridges with the given Location, if found, and an error.
-func (s *Storage) GetBridgesByLocation(location string) ([]models.Bridge, error) {
-	var bridges []models.Bridge
-	err := s.db.Find("Location", location, &bridges)
-	return bridges, err
-}
-
-// DeleteBridge accepts a name and an ID. If you want to delete by name, pass 0 as the ID;
-// if you want to delete by ID, pass the empty string as name.
-func (s *Storage) DeleteBridge(id int, name string) error {
-	if id == 0 && name == "" {
-		return fmt.Errorf("need to pass id or name")
-	}
-	var bridge models.Bridge
-	var err error
-	switch {
-	case id != 0:
-		bridge, err = s.GetBridgeByID(id)
-		if err != nil {
-			return err
-		}
-	case name != "":
-		bridge, err = s.GetBridgeByName(name)
-		if err != nil {
-			return err
-		}
-	default:
-		return fmt.Errorf("unhandled case")
-	}
-	return s.db.DeleteStruct(&bridge)
-}
-
-// Close closes the db connection
-func (s *Storage) Close() {
-	s.db.Close()
-}
-
-// MaybeUpdateLastUsedForIntroducer will attempt to update the LastUsed timestamp for the introducer
-// that matches the passed URL.
-func MaybeUpdateLastUsedForIntroducer(url string) error {
-	db, err := NewStorageWithDefaultDir()
-	if err != nil {
-		return err
-	}
-	defer db.Close()
-
-	var intro models.Introducer
-	err = db.db.One("URL", url, &intro)
-	if err != nil {
-		log.Error().Err(err)
-		return err
-	}
-	intro.LastUsed = time.Now()
-	err = db.db.Save(&intro)
-	if err != nil {
-		return err
-	}
-	return nil
-}
diff --git a/vendor/0xacab.org/leap/bitmask-core/pkg/storage/db_store.go b/vendor/0xacab.org/leap/bitmask-core/pkg/storage/db_store.go
new file mode 100644
index 0000000000000000000000000000000000000000..6794198dd055f364e418228f9174be5af4bccdc3
--- /dev/null
+++ b/vendor/0xacab.org/leap/bitmask-core/pkg/storage/db_store.go
@@ -0,0 +1,161 @@
+package storage
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+
+	"github.com/asdine/storm/v3"
+)
+
+type DBStore struct {
+	Store
+	db       *storm.DB
+	filepath string
+}
+
+const defaultBucket = "defaultBucket"
+
+func NewDBStore(path string) (*DBStore, error) {
+	fp := filepath.Join(path, "bitmask.db")
+	err := os.MkdirAll(filepath.Dir(fp), 0750)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create DB store path: %v", err)
+	}
+	db, err := storm.Open(fp)
+
+	if err != nil {
+		return nil, fmt.Errorf("failed to open storm DB: %v", err)
+	}
+
+	return &DBStore{db: db, filepath: fp}, nil
+}
+
+func (s *DBStore) GetString(key string) string {
+	var value string
+	err := s.db.Get(defaultBucket, key, &value)
+	if err != nil {
+		return ""
+	}
+	return value
+}
+
+func (s *DBStore) GetStringWithDefault(key string, value string) string {
+	var result string
+	err := s.db.Get(defaultBucket, key, &value)
+	if err != nil {
+		return value
+	}
+	return result
+}
+
+func (s *DBStore) GetBoolean(key string) bool {
+	var value bool
+	err := s.db.Get(defaultBucket, key, &value)
+	if err != nil {
+		return false
+	}
+	return value
+}
+
+func (s *DBStore) GetBooleanWithDefault(key string, value bool) bool {
+	var result bool
+	err := s.db.Get(defaultBucket, key, &result)
+	if err != nil {
+		return value
+	}
+	return result
+}
+
+func (s *DBStore) GetInt(key string) int {
+	var value int
+	err := s.db.Get(defaultBucket, key, &value)
+	if err != nil {
+		return 0
+	}
+	return value
+}
+func (s *DBStore) GetIntWithDefault(key string, value int) int {
+	var result int
+	err := s.db.Get(defaultBucket, key, &result)
+	if err != nil {
+		return value
+	}
+	return result
+}
+func (s *DBStore) GetLong(key string) int64 {
+	var value int64
+	err := s.db.Get(defaultBucket, key, &value)
+	if err != nil {
+		return 0
+	}
+	return value
+}
+
+func (s *DBStore) GetLongWithDefault(key string, value int64) int64 {
+	var result int64
+	err := s.db.Get(defaultBucket, key, &result)
+	if err != nil {
+		return value
+	}
+	return result
+}
+
+func (s *DBStore) GetByteArray(key string) []byte {
+	var value []byte
+	err := s.db.Get(defaultBucket, key, &value)
+	if err != nil {
+		return nil
+	}
+	return value
+}
+
+func (s *DBStore) GetByteArrayWithDefault(key string, value []byte) []byte {
+	var result []byte
+	err := s.db.Get(defaultBucket, key, &result)
+	if err != nil {
+		return value
+	}
+	return result
+}
+
+// Key-Value Setters
+func (s *DBStore) SetString(key string, value string) {
+	_ = s.db.Set(defaultBucket, key, value)
+}
+func (s *DBStore) SetBoolean(key string, value bool) {
+	_ = s.db.Set(defaultBucket, key, value)
+}
+
+func (s *DBStore) SetInt(key string, value int) {
+	_ = s.db.Set(defaultBucket, key, value)
+}
+func (s *DBStore) SetLong(key string, value int64) {
+	_ = s.db.Set(defaultBucket, key, value)
+}
+func (s *DBStore) SetByteArray(key string, value []byte) {
+	_ = s.db.Set(defaultBucket, key, value)
+}
+
+func (s *DBStore) Contains(key string) (bool, error) {
+	return s.db.KeyExists(defaultBucket, key)
+}
+func (s *DBStore) Remove(key string) error {
+	return s.db.Delete(defaultBucket, key)
+}
+func (s *DBStore) Clear() error {
+	return s.db.Drop(defaultBucket)
+}
+
+func (s *DBStore) Close() error {
+	return s.db.Close()
+}
+
+func (s *DBStore) Open() error {
+	db, err := storm.Open(s.filepath)
+	if err != nil {
+		return fmt.Errorf("failed to open database: %v", err)
+	}
+	s.db = db
+	return nil
+}
diff --git a/vendor/0xacab.org/leap/bitmask-core/pkg/storage/introducer.go b/vendor/0xacab.org/leap/bitmask-core/pkg/storage/introducer.go
deleted file mode 100644
index d7dca644c5dcb52943dd2f4c5a36fa06e89d3d08..0000000000000000000000000000000000000000
--- a/vendor/0xacab.org/leap/bitmask-core/pkg/storage/introducer.go
+++ /dev/null
@@ -1,34 +0,0 @@
-package storage
-
-import (
-	"strings"
-
-	"github.com/rs/zerolog/log"
-)
-
-func MaybeGetIntroducerURLByName(introducerName string) string {
-	if introducerName == "" {
-		return ""
-	}
-	switch strings.HasPrefix(introducerName, "obfsvpnintro://") {
-	// does not have prefix schema, let's treat it as a name in the internal storage
-	case false:
-		db, err := NewStorageWithDefaultDir()
-		if err != nil {
-			log.Fatal().Err(err).Msg("cannot open storage")
-		}
-		defer db.Close()
-
-		_i, err := db.GetIntroducerByName(introducerName)
-		if err != nil {
-			log.Fatal().Err(err).Msg("cannot get introducer by name")
-		}
-		// We got a valid (and unique) introducer from storage, so that
-		// we can retrieve the URL value. This is assumed to have been validated
-		// when introducing it, but I should be more paranoid here.
-		// TODO(atanarjuat): be more paranoid and validate the URL/signature etc.
-		return _i.URL
-	default:
-		return ""
-	}
-}
diff --git a/vendor/0xacab.org/leap/bitmask-core/pkg/storage/storage.go b/vendor/0xacab.org/leap/bitmask-core/pkg/storage/storage.go
new file mode 100644
index 0000000000000000000000000000000000000000..b0a66d3ac0c1f47fa24ec07c36b08fb68d2fdd60
--- /dev/null
+++ b/vendor/0xacab.org/leap/bitmask-core/pkg/storage/storage.go
@@ -0,0 +1,460 @@
+// storage provides an embeddable database that bitmask uses to persist
+// a series of values.
+// For now, we store the following items in the database:
+// - Introducer metadata.
+// - Private bridges.
+//
+// Example Usage to initilialize the default storage struct:
+//
+//	InitAppStorage()
+//
+// To work with the initialized storage call:
+//
+//	GetStorage()
+//
+
+package storage
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/url"
+	"os"
+	"path/filepath"
+	"time"
+
+	homedir "github.com/mitchellh/go-homedir"
+
+	"0xacab.org/leap/bitmask-core/pkg/models"
+)
+
+var AppName = "bitmask"
+var appStorage *Storage
+
+const (
+	INTRODUCER  = "INTRODUCER"
+	BRIDGE      = "BRIDGE"
+	COUNTRYCODE = "COUNTRYCODE"
+)
+
+type Storage struct {
+	store Store
+}
+
+// CompareIntroducer is a function type for the comparison of introducers
+type CompareIntroducer func(introducer models.Introducer) bool
+
+// CompareBridge is a function type for the comparison of bridges
+type CompareBridge func(bridge models.Bridge) bool
+
+// InitAppStorage initializes the global storage with a given storage instance
+func InitAppStorageWith(store Store) {
+	if appStorage != nil {
+		appStorage.Close()
+	}
+	appStorage = NewStorageWithStore(store)
+}
+
+// Init AppStorage initializes the global storage with the default storage directory
+func InitAppStorage() error {
+	if appStorage != nil {
+		appStorage.Close()
+	}
+	storage, err := NewStorageWithDefaultDir()
+	if err != nil {
+		return fmt.Errorf("failed to initialize app storage: %v", err)
+	}
+	appStorage = storage
+	return nil
+}
+
+// NewStorage initializes a new storage with a given path.
+// `NewStorageWithDefaultDir` should be preferred to initialize a storage, since it will try to pick a default path.
+func NewStorage(path string) (*Storage, error) {
+	store, err := NewDBStore(path)
+	if err != nil {
+		return nil, err
+	}
+	return &Storage{
+		store: store,
+	}, nil
+}
+
+// NewStorageWithStore initializes the storage struct with a custom store. This
+// can be used to implement custom storage adapters e.g. for different database types
+func NewStorageWithStore(store Store) *Storage {
+	return &Storage{
+		store: store,
+	}
+}
+
+// NewStoreageWithDefaultDir initializes a storage struct with a storm DB
+// and looks up the correct default paths for Linux, MacOS and Windows.
+// Don't call this method in context of mobile app development
+func NewStorageWithDefaultDir() (*Storage, error) {
+	home, err := homedir.Dir()
+	if err != nil {
+		return nil, err
+	}
+
+	var configPath string
+	switch _os := os.Getenv("GOOS"); _os {
+	case "windows":
+		configPath = filepath.Join(os.Getenv("APPDATA"), AppName)
+	default:
+		// This will cover both 'darwin' (macOS) and 'linux'
+		configPath = filepath.Join(home, ".config", AppName)
+	}
+
+	err = os.MkdirAll(configPath, 0700)
+	if err != nil {
+		return nil, err
+	}
+
+	return NewStorage(configPath)
+}
+
+// AddIntroducers adds a new Introducer model from URL
+// to the storage
+func (s *Storage) AddIntroducer(url string) error {
+	fqdn, err := getFQDNFromIntroducerURL(url)
+	if err != nil {
+		return err
+	}
+
+	item := &models.Introducer{
+		FQDN:      *fqdn,
+		URL:       url,
+		CreatedAt: time.Now(),
+	}
+
+	// delete existing introducer with same FQDN as we only want
+	// to allow one for per provider for now
+	err = s.DeleteIntroducer(*fqdn)
+	if err != nil {
+		return err
+	}
+
+	introducers, err := s.getAllIntroducers()
+
+	if err != nil {
+		return err
+	}
+	introducers = append(introducers, *item)
+	return s.saveIntroducers(introducers)
+}
+
+func (s *Storage) getAllIntroducers() ([]models.Introducer, error) {
+	// Create an empty slice of Introducer
+	emptySlice := []models.Introducer{}
+	bytes, _ := json.Marshal(emptySlice)
+
+	introducerString := s.store.GetByteArrayWithDefault(INTRODUCER, bytes)
+	introducers, err := unmarshalJSON[[]models.Introducer](introducerString)
+	if err != nil {
+		return nil, err
+	}
+	return *introducers, err
+}
+
+func (s *Storage) saveIntroducers(introducers []models.Introducer) error {
+	bytes, err := json.Marshal(introducers)
+	if err != nil {
+		return err
+	}
+	s.store.SetByteArray(INTRODUCER, bytes)
+	return nil
+}
+
+func unmarshalJSON[T any](data []byte) (*T, error) {
+	var result T
+	err := json.Unmarshal(data, &result)
+	if err != nil {
+		return nil, err
+	}
+	return &result, nil
+}
+
+// ListIntroducers returns an array of all introducers
+func (s *Storage) ListIntroducers() ([]models.Introducer, error) {
+	return s.getAllIntroducers()
+}
+
+// GetIntroducerByFQDN returns the first introducer for a given fqdn.
+func (s *Storage) GetIntroducerByFQDN(fqdn string) (*models.Introducer, error) {
+	compare := func(intro models.Introducer) bool {
+		return intro.FQDN == fqdn
+	}
+	return s.getFirstIntroducer(compare)
+}
+
+// GetIntroducerByURL will return the Introducer with the given URL, if found, and an error.
+func (s *Storage) GetIntroducerByURL(url string) (*models.Introducer, error) {
+	compare := func(intro models.Introducer) bool {
+		return intro.URL == url
+	}
+	return s.getFirstIntroducer(compare)
+}
+
+func (s *Storage) getFirstIntroducer(compare CompareIntroducer) (*models.Introducer, error) {
+	introducers, err := s.getAllIntroducers()
+	if err != nil {
+		return nil, err
+	}
+	for _, intro := range introducers {
+		if compare(intro) {
+			return &intro, nil // Return a pointer to the found Introducer
+		}
+	}
+	return nil, fmt.Errorf("introducer not found")
+}
+
+func (s *Storage) updateFirstIntroducer(compare CompareIntroducer, lastUsed time.Time) ([]models.Introducer, error) {
+	introducers, err := s.getAllIntroducers()
+	if err != nil {
+		return nil, err
+	}
+
+	found := false
+	for _, intro := range introducers {
+		if compare(intro) {
+			intro.LastUsed = lastUsed
+			found = true
+			break
+		}
+	}
+
+	if !found {
+		return nil, fmt.Errorf("introducer not found")
+	}
+
+	return introducers, nil
+}
+
+// DeleteIntroducer deletes all introducers for a given fqdn.
+func (s *Storage) DeleteIntroducer(fqdn string) error {
+	if fqdn == "" {
+		return errors.New("need to pass fully qualified domain name of the introducer")
+	}
+	var introducers, err = s.getAllIntroducers()
+	if err != nil {
+		return err
+	}
+
+	compare := func(intro models.Introducer) bool {
+		return fqdn == intro.FQDN
+	}
+
+	updatedIntroducers := func() []models.Introducer {
+		for i, intro := range introducers {
+			if compare(intro) {
+				// Swap with the last element
+				lastIndex := len(introducers) - 1
+				introducers[i] = introducers[lastIndex] // Move the last element to the current position
+				return introducers[:lastIndex]          // Return the truncated slice
+			}
+		}
+		return introducers // Return original slice if ID not found
+	}()
+
+	return s.saveIntroducers(updatedIntroducers)
+}
+
+// NewBridge creates a new Bridge from the passed parameters.
+func (s *Storage) NewBridge(name, bridgeType, location, raw string) error {
+	item := &models.Bridge{
+		Name:     name,
+		Type:     bridgeType,
+		Location: location,
+		Raw:      raw,
+	}
+	item.CreatedAt = time.Now()
+	bridges, err := s.getAllBridges()
+	if err != nil {
+		return err
+	}
+
+	comparison := func(intro models.Bridge) bool {
+		return intro.Name == name || intro.Raw == raw
+	}
+	bridge, _ := s.getFirstBridge(comparison)
+	if bridge != nil {
+		return fmt.Errorf("bridge %v already saved", bridge)
+	}
+
+	bridges = append(bridges, *item)
+	return s.saveBridges(bridges)
+}
+
+func (s *Storage) getAllBridges() ([]models.Bridge, error) {
+	// Create an empty slice of Introducer
+	emptySlice := []models.Bridge{}
+	bytes, _ := json.Marshal(emptySlice)
+
+	bridgeBytes := s.store.GetByteArrayWithDefault(BRIDGE, bytes)
+	bridges, err := unmarshalJSON[[]models.Bridge](bridgeBytes)
+	if err != nil {
+		return nil, err
+	}
+	return *bridges, err
+}
+
+func (s *Storage) saveBridges(bridges []models.Bridge) error {
+	bytes, err := json.Marshal(bridges)
+	if err != nil {
+		return err
+	}
+	s.store.SetByteArray(BRIDGE, bytes)
+	return nil
+}
+
+// ListBridges returns an array of all the bridges.
+func (s *Storage) ListBridges() ([]models.Bridge, error) {
+	return s.getAllBridges()
+}
+
+func (s *Storage) getFirstBridge(compare CompareBridge) (*models.Bridge, error) {
+	bridges, err := s.getAllBridges()
+	if err != nil {
+		return nil, err
+	}
+	for _, bridge := range bridges {
+		if compare(bridge) {
+			return &bridge, nil // Return a pointer to the found Bridge
+		}
+	}
+	return nil, fmt.Errorf("bridge not found")
+}
+
+func (s *Storage) getBridges(compare CompareBridge) ([]models.Bridge, error) {
+	bridges, err := s.getAllBridges()
+	if err != nil {
+		return nil, err
+	}
+	var result []models.Bridge
+
+	for _, bridge := range bridges {
+		if compare(bridge) {
+			result = append(result, bridge)
+		}
+	}
+
+	if len(result) == 0 {
+		return nil, fmt.Errorf("bridge not found")
+	}
+
+	return result, nil
+}
+
+// GetBridgeByName will return the Bridge with the given Name, if found, and an error.
+func (s *Storage) GetBridgeByName(name string) (*models.Bridge, error) {
+	compare := func(bridge models.Bridge) bool {
+		return bridge.Name == name
+	}
+	return s.getFirstBridge(compare)
+}
+
+// GetBridgesByType will return all Bridges with the given Type, if found, and an error.
+func (s *Storage) GetBridgesByType(bridgeType string) ([]models.Bridge, error) {
+	compare := func(bridge models.Bridge) bool {
+		return bridge.Type == bridgeType
+	}
+	return s.getBridges(compare)
+}
+
+// GetBridgesByLocation will return all Bridges with the given Location, if found, and an error.
+func (s *Storage) GetBridgesByLocation(location string) ([]models.Bridge, error) {
+	compare := func(bridge models.Bridge) bool {
+		return bridge.Location == location
+	}
+	return s.getBridges(compare)
+}
+
+// DeleteBridge accepts a name and an ID. If you want to delete by name, pass 0 as the ID;
+// if you want to delete by ID, pass the empty string as name.
+func (s *Storage) DeleteBridge(name string) error {
+	bridges, err := s.getAllBridges()
+	if err != nil {
+		return err
+	}
+
+	compare := func(bridge models.Bridge) bool {
+		return bridge.Name == name
+	}
+
+	updatedBridges := func() []models.Bridge {
+		for i, bridge := range bridges {
+			if compare(bridge) {
+				// Swap with the last element
+				lastIndex := len(bridges) - 1
+				bridges[i] = bridges[lastIndex] // Move the last element to the current position
+				return bridges[:lastIndex]      // Return the truncated slice
+			}
+		}
+		return bridges // Return original slice if ID not found
+	}()
+
+	return s.saveBridges(updatedBridges)
+
+}
+
+// Close closes the db connection
+func (s *Storage) Close() {
+	_ = s.store.Close()
+}
+
+// GetStorage returns an initialized storage instance
+// if the global appStorage was initialized it has precedence
+func GetStorage() (*Storage, error) {
+	var storage *Storage
+	var err error
+	if appStorage == nil {
+		storage, err = NewStorageWithDefaultDir()
+		if err != nil {
+			return nil, err
+		}
+	} else {
+		storage = appStorage
+	}
+	return storage, nil
+
+}
+
+func getFQDNFromIntroducerURL(introducerURL string) (*string, error) {
+	// Parse the introducer URL string
+	u, err := url.Parse(introducerURL)
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse introducer URL: %w", err)
+	}
+
+	// Extract FQDN from query parameters
+	fqdn := u.Query().Get("fqdn")
+	if fqdn == "" {
+		return nil, fmt.Errorf("FQDN not found in the introducer URL")
+	}
+
+	return &fqdn, nil
+}
+
+// UpdateLastUsedForIntroducer will attempt to update the LastUsed timestamp for the introducer
+// that matches the passed URL.
+func (s *Storage) UpdateLastUsedForIntroducer(url string) error {
+	var compare CompareIntroducer = func(introducer models.Introducer) bool {
+		return introducer.URL == url
+	}
+	introducers, err := s.updateFirstIntroducer(compare, time.Now())
+	if err != nil {
+		return err
+	}
+
+	return s.saveIntroducers(introducers)
+}
+
+func (s *Storage) SaveFallbackCountryCode(cc string) {
+	s.store.SetString(COUNTRYCODE, cc)
+}
+
+func (s *Storage) GetFallbackCountryCode() string {
+	return s.store.GetString(COUNTRYCODE)
+}
diff --git a/vendor/0xacab.org/leap/bitmask-core/pkg/storage/store.go b/vendor/0xacab.org/leap/bitmask-core/pkg/storage/store.go
new file mode 100644
index 0000000000000000000000000000000000000000..85ec2c53df67c0b07f94cf766ce1ccc68e88a126
--- /dev/null
+++ b/vendor/0xacab.org/leap/bitmask-core/pkg/storage/store.go
@@ -0,0 +1,28 @@
+package storage
+
+type Store interface {
+	// Key-Value Getters
+	GetString(key string) string
+	GetStringWithDefault(key string, value string) string
+	GetBoolean(key string) bool
+	GetBooleanWithDefault(key string, value bool) bool
+	GetInt(key string) int
+	GetIntWithDefault(key string, value int) int
+	GetLong(key string) int64
+	GetLongWithDefault(key string, value int64) int64
+	GetByteArray(key string) []byte
+	GetByteArrayWithDefault(key string, value []byte) []byte
+
+	// Key-Value Setters
+	SetString(key string, value string)
+	SetBoolean(key string, value bool)
+	SetInt(key string, value int)
+	SetLong(key string, value int64)
+	SetByteArray(key string, value []byte)
+
+	Open() error
+	Close() error
+	Contains(key string) (bool, error)
+	Remove(key string) error
+	Clear() error
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 823605be4edca90513ef1056218944526a3d5996..fa83a78a184fccae88220c9e5d3709aef058d1bf 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -1,4 +1,4 @@
-# 0xacab.org/leap/bitmask-core v0.0.0-20240830161617-121267e4c4eb
+# 0xacab.org/leap/bitmask-core v0.0.0-20241015185856-0812b9aadf98
 ## explicit; go 1.22.2
 0xacab.org/leap/bitmask-core/models
 0xacab.org/leap/bitmask-core/pkg/bootstrap