From c9163f221d4cc7b167b5f68eb51ce09843939352 Mon Sep 17 00:00:00 2001
From: "Kali Kaneko (leap communications)" <kali@leap.se>
Date: Mon, 17 Dec 2018 23:48:38 +0100
Subject: [PATCH] [feat] use geolocated gateways

we try to use the geoip service, and if the answer has an entry for the
sorted gateways, we just use it instead of using the timezone heuristic.

- Resolves: #84
---
 .gitlab-ci.yml         |  1 +
 standalone/bonafide.go | 81 ++++++++++++++++++++++++++++++++++--------
 2 files changed, 68 insertions(+), 14 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index b3f1861f..7a39e594 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -38,6 +38,7 @@ win_installer:
    - git clone https://github.com/AllenDang/w32
    - cd w32
    - curl https://downloads.leap.se/thirdparty/w32.patch | patch -p1 -N
+
    - cd ${APP_PATH}
 
    - git clone https://0xacab.org/leap/riseup_vpn
diff --git a/standalone/bonafide.go b/standalone/bonafide.go
index e37c332b..e28eb089 100644
--- a/standalone/bonafide.go
+++ b/standalone/bonafide.go
@@ -34,6 +34,7 @@ import (
 const (
 	certAPI        = "https://api.black.riseup.net/1/cert"
 	eipAPI         = "https://api.black.riseup.net/1/config/eip-service.json"
+	geolocationAPI = "https://api.black.riseup.net/getmyip/json"
 	secondsPerHour = 60 * 60
 )
 
@@ -78,6 +79,7 @@ type bonafide struct {
 	eip            *eipService
 	defaultGateway string
 }
+
 type httpClient interface {
 	Post(url, contentType string, body io.Reader) (resp *http.Response, err error)
 }
@@ -103,6 +105,20 @@ type gateway struct {
 	Location  string
 }
 
+type gatewayDistance struct {
+	gateway  gateway
+	distance int
+}
+
+type geoLocation struct {
+	IPAddress      string   `json:"ip"`
+	Country        string   `json:"cc"`
+	City           string   `json:"city"`
+	Latitude       float64  `json:"lat"`
+	Longitude      float64  `json:"lon"`
+	SortedGateways []string `json:"gateways"`
+}
+
 func newBonafide() *bonafide {
 	certs := x509.NewCertPool()
 	certs.AppendCertsFromPEM(caCert)
@@ -172,12 +188,34 @@ func (b *bonafide) getOpenvpnArgs() ([]string, error) {
 				args = append(args, "--"+arg)
 			}
 		default:
-			log.Printf("Uknwon openvpn argument type: %s - %v", arg, value)
+			log.Printf("Unknown openvpn argument type: %s - %v", arg, value)
 		}
 	}
 	return args, nil
 }
 
+func (b *bonafide) fetchGeolocation() ([]string, error) {
+	resp, err := b.client.Post(geolocationAPI, "", nil)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+	if resp.StatusCode != 200 {
+		return nil, fmt.Errorf("get geolocation failed with status: %s", resp.Status)
+	}
+
+	geo := &geoLocation{}
+	dataJSON, err := ioutil.ReadAll(resp.Body)
+	err = json.Unmarshal(dataJSON, &geo)
+	if err != nil {
+		_ = fmt.Errorf("get vpn cert has failed with status: %s", resp.Status)
+		return nil, err
+	}
+
+	return geo.SortedGateways, nil
+
+}
+
 func (b *bonafide) fetchEipJSON() error {
 	resp, err := b.client.Post(eipAPI, "", nil)
 	if err != nil {
@@ -200,13 +238,22 @@ func (b *bonafide) fetchEipJSON() error {
 	return nil
 }
 
-func (b *bonafide) sortGateways() {
-	type gatewayDistance struct {
-		gateway  gateway
-		distance int
+func (b *bonafide) sortGatewaysByGeolocation(geolocatedGateways []string) []gatewayDistance {
+	gws := []gatewayDistance{}
+
+	for i, host := range geolocatedGateways {
+		for _, gw := range b.eip.Gateways {
+			if gw.Host == host {
+				gws = append(gws, gatewayDistance{gw, i})
+			}
+		}
 	}
+	return gws
+}
 
+func (b *bonafide) sortGatewaysByTimezone() []gatewayDistance {
 	gws := []gatewayDistance{}
+
 	for _, gw := range b.eip.Gateways {
 		distance := 13
 		if gw.Location == b.defaultGateway {
@@ -221,23 +268,29 @@ func (b *bonafide) sortGateways() {
 		}
 		gws = append(gws, gatewayDistance{gw, distance})
 	}
-
 	rand.Seed(time.Now().UnixNano())
 	cmp := func(i, j int) bool {
 		if gws[i].distance == gws[j].distance {
-			// TODO: a hack to distribute more the load into the new gw.
-			//       Let's delete it as soon as is more spread the load.
-			if gws[i].gateway.Host == "giraffe.riseup.net" {
-				return rand.Intn(4) != 0
-			} else if gws[j].gateway.Host == "giraffe.riseup.net" {
-				return rand.Intn(4) == 0
-			}
-
 			return rand.Intn(2) == 1
 		}
 		return gws[i].distance < gws[j].distance
 	}
 	sort.Slice(gws, cmp)
+	return gws
+}
+
+func (b *bonafide) sortGateways() {
+	gws := []gatewayDistance{}
+
+	geolocatedGateways, _ := b.fetchGeolocation()
+
+	if len(geolocatedGateways) > 0 {
+		gws = b.sortGatewaysByGeolocation(geolocatedGateways)
+	} else {
+		log.Printf("Falling back to timezone heuristic for gateway selection")
+		gws = b.sortGatewaysByTimezone()
+	}
+
 	for i, gw := range gws {
 		b.eip.Gateways[i] = gw.gateway
 	}
-- 
GitLab