Skip to content
Snippets Groups Projects
Unverified Commit 35aaba1e authored by meskio's avatar meskio :tent:
Browse files

[refactor] bonafide to parse eip-service.json v3

parent 78ee21c4
No related branches found
No related tags found
No related merge requests found
......@@ -13,7 +13,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package standalone
package bonafide
import (
"crypto/tls"
......@@ -23,11 +23,7 @@ import (
"io"
"io/ioutil"
"log"
"math/rand"
"net/http"
"sort"
"strconv"
"strings"
"time"
"0xacab.org/leap/bitmask-vpn/pkg/config"
......@@ -35,46 +31,29 @@ import (
const (
certAPI = config.APIURL + "1/cert"
eipAPI = config.APIURL + "1/config/eip-service.json"
secondsPerHour = 60 * 60
retryFetchJSONSeconds = 15
)
type bonafide struct {
client httpClient
tzOffsetHours int
eip *eipService
defaultGateway string
type Bonafide struct {
client httpClient
eip *eipService
tzOffsetHours int
}
type httpClient interface {
Post(url, contentType string, body io.Reader) (resp *http.Response, err error)
}
type eipService struct {
Gateways []gateway
Locations map[string]struct {
CountryCode string
Hemisphere string
Name string
Timezone string
}
OpenvpnConfiguration map[string]interface{} `json:"openvpn_configuration"`
}
type gateway struct {
Capabilities struct {
Ports []string
Protocols []string
}
type Gateway struct {
Host string
IPAddress string `json:"ip_address"`
IPAddress string
Location string
Ports []string
Protocols []string
Options map[string]string
}
type gatewayDistance struct {
gateway gateway
distance int
type openvpnConfig map[string]interface{}
type httpClient interface {
Post(url, contentType string, body io.Reader) (resp *http.Response, err error)
}
type geoLocation struct {
......@@ -86,7 +65,7 @@ type geoLocation struct {
SortedGateways []string `json:"gateways"`
}
func newBonafide() *bonafide {
func New() *Bonafide {
certs := x509.NewCertPool()
certs.AppendCertsFromPEM(config.CaCert)
client := &http.Client{
......@@ -99,15 +78,14 @@ func newBonafide() *bonafide {
_, tzOffsetSeconds := time.Now().Zone()
tzOffsetHours := tzOffsetSeconds / secondsPerHour
return &bonafide{
client: client,
tzOffsetHours: tzOffsetHours,
eip: nil,
defaultGateway: "",
return &Bonafide{
client: client,
eip: nil,
tzOffsetHours: tzOffsetHours,
}
}
func (b *bonafide) getCertPem() ([]byte, error) {
func (b *Bonafide) GetCertPem() ([]byte, error) {
resp, err := b.client.Post(certAPI, "", nil)
if err != nil {
return nil, err
......@@ -120,7 +98,7 @@ func (b *bonafide) getCertPem() ([]byte, error) {
return ioutil.ReadAll(resp.Body)
}
func (b *bonafide) getGateways() ([]gateway, error) {
func (b *Bonafide) GetGateways(transport string) ([]Gateway, error) {
if b.eip == nil {
err := b.fetchEipJSON()
if err != nil {
......@@ -128,40 +106,25 @@ func (b *bonafide) getGateways() ([]gateway, error) {
}
}
return b.eip.Gateways, nil
return b.eip.getGateways(transport), nil
}
func (b *bonafide) setDefaultGateway(name string) {
b.defaultGateway = name
func (b *Bonafide) SetDefaultGateway(name string) {
b.eip.setDefaultGateway(name)
b.sortGateways()
}
func (b *bonafide) getOpenvpnArgs() ([]string, error) {
func (b *Bonafide) GetOpenvpnArgs() ([]string, error) {
if b.eip == nil {
err := b.fetchEipJSON()
if err != nil {
return nil, err
}
}
args := []string{}
for arg, value := range b.eip.OpenvpnConfiguration {
switch v := value.(type) {
case string:
args = append(args, "--"+arg)
args = append(args, strings.Split(v, " ")...)
case bool:
if v {
args = append(args, "--"+arg)
}
default:
log.Printf("Unknown openvpn argument type: %s - %v", arg, value)
}
}
return args, nil
return b.eip.getOpenvpnArgs(), nil
}
func (b *bonafide) fetchGeolocation() ([]string, error) {
func (b *Bonafide) fetchGeolocation() ([]string, error) {
resp, err := b.client.Post(config.GeolocationAPI, "", nil)
if err != nil {
return nil, err
......@@ -183,98 +146,13 @@ func (b *bonafide) fetchGeolocation() ([]string, error) {
}
func (b *bonafide) fetchEipJSON() error {
resp, err := b.client.Post(eipAPI, "", nil)
for err != nil {
log.Printf("Error fetching eip json: %v", err)
time.Sleep(retryFetchJSONSeconds * time.Second)
resp, err = b.client.Post(eipAPI, "", nil)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("get eip json has failed with status: %s", resp.Status)
}
var eip eipService
decoder := json.NewDecoder(resp.Body)
err = decoder.Decode(&eip)
if err != nil {
return err
}
b.eip = &eip
b.sortGateways()
return nil
}
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 {
distance = -1
} else {
gwOffset, err := strconv.Atoi(b.eip.Locations[gw.Location].Timezone)
if err != nil {
log.Printf("Error sorting gateways: %v", err)
} else {
distance = tzDistance(b.tzOffsetHours, gwOffset)
}
}
gws = append(gws, gatewayDistance{gw, distance})
}
rand.Seed(time.Now().UnixNano())
cmp := func(i, j int) bool {
if gws[i].distance == gws[j].distance {
return rand.Intn(2) == 1
}
return gws[i].distance < gws[j].distance
}
sort.Slice(gws, cmp)
return gws
}
func (b *bonafide) sortGateways() {
gws := []gatewayDistance{}
func (b *Bonafide) sortGateways() {
geolocatedGateways, _ := b.fetchGeolocation()
if len(geolocatedGateways) > 0 {
gws = b.sortGatewaysByGeolocation(geolocatedGateways)
b.eip.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
}
}
func tzDistance(offset1, offset2 int) int {
abs := func(x int) int {
if x < 0 {
return -x
}
return x
}
distance := abs(offset1 - offset2)
if distance > 12 {
distance = 24 - distance
b.eip.sortGatewaysByTimezone(b.tzOffsetHours)
}
return distance
}
......@@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package standalone
package bonafide
import (
"bytes"
......@@ -27,8 +27,8 @@ var (
)
func TestIntegrationGetCert(t *testing.T) {
b := newBonafide()
cert, err := b.getCertPem()
b := New()
cert, err := b.GetCertPem()
if err != nil {
t.Fatal("getCert returned an error: ", err)
}
......@@ -43,8 +43,8 @@ func TestIntegrationGetCert(t *testing.T) {
}
func TestGetGateways(t *testing.T) {
b := newBonafide()
gateways, err := b.getGateways()
b := New()
gateways, err := b.GetGateways("openvpn")
if err != nil {
t.Fatal("getGateways returned an error: ", err)
}
......
......@@ -13,7 +13,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package standalone
package bonafide
import (
"io"
......@@ -21,12 +21,15 @@ import (
"net/http"
"os"
"reflect"
"strconv"
"strings"
"testing"
)
const (
certPath = "testdata/cert"
eipPath = "testdata/eip-service.json"
eip1Path = "testdata/eip-service.json"
eipPath = "testdata/eip-service3.json"
)
type client struct {
......@@ -42,8 +45,8 @@ func (c client) Post(url, contentType string, body io.Reader) (resp *http.Respon
}
func TestGetCert(t *testing.T) {
b := bonafide{client: client{certPath}}
cert, err := b.getCertPem()
b := Bonafide{client: client{certPath}}
cert, err := b.GetCertPem()
if err != nil {
t.Fatal("getCert returned an error: ", err)
}
......@@ -79,11 +82,11 @@ func TestGatewayTzLocation(t *testing.T) {
}
for tzOffset, location := range values {
b := bonafide{
b := Bonafide{
client: client{eipPath},
tzOffsetHours: tzOffset,
}
gateways, err := b.getGateways()
gateways, err := b.GetGateways("openvpn")
if err != nil {
t.Errorf("getGateways returned an error: %v", err)
continue
......@@ -98,3 +101,120 @@ func TestGatewayTzLocation(t *testing.T) {
}
}
}
func TestOpenvpnGateways(t *testing.T) {
b := Bonafide{
client: client{eipPath},
}
gateways, err := b.GetGateways("openvpn")
if err != nil {
t.Fatalf("getGateways returned an error: %v", err)
}
if len(gateways) == 0 {
t.Fatalf("No obfs4 gateways found")
}
present := make([]bool, 6)
for _, g := range gateways {
i, err := strconv.Atoi(g.Host[0:1])
if err != nil {
t.Fatalf("unkonwn host %s: %v", g.Host, err)
}
present[i] = true
}
for i, p := range present {
switch i {
case 0:
continue
case 5:
if p {
t.Errorf("Host %d should not have obfs4 transport", i)
}
default:
if !p {
t.Errorf("Host %d should have obfs4 transport", i)
}
}
}
}
func TestObfs4Gateways(t *testing.T) {
b := Bonafide{
client: client{eipPath},
}
gateways, err := b.GetGateways("obfs4")
if err != nil {
t.Fatalf("getGateways returned an error: %v", err)
}
if len(gateways) == 0 {
t.Fatalf("No obfs4 gateways found")
}
present := make([]bool, 6)
for _, g := range gateways {
i, err := strconv.Atoi(g.Host[0:1])
if err != nil {
t.Fatalf("unkonwn host %s: %v", g.Host, err)
}
present[i] = true
cert, ok := g.Options["cert"]
if !ok {
t.Fatalf("No cert in options (%s): %v", g.Host, g.Options)
}
if cert != "obfs-cert" {
t.Errorf("No valid options given (%s): %v", g.Host, g.Options)
}
}
for i, p := range present {
switch i {
case 0:
continue
case 2, 4:
if p {
t.Errorf("Host %d should not have obfs4 transport", i)
}
default:
if !p {
t.Errorf("Host %d should have obfs4 transport", i)
}
}
}
}
type fallClient struct {
path string
}
func (c fallClient) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) {
statusCode := 200
if strings.Contains(url, "3/config/eip-service.json") {
statusCode = 404
}
f, err := os.Open(c.path)
return &http.Response{
Body: f,
StatusCode: statusCode,
}, err
}
func TestEipServiceV1Fallback(t *testing.T) {
b := Bonafide{
client: fallClient{eip1Path},
}
gateways, err := b.GetGateways("obfs4")
if err != nil {
t.Fatalf("getGateways obfs4 returned an error: %v", err)
}
if len(gateways) != 0 {
t.Fatalf("It found some obfs4 gateways: %v", gateways)
}
gateways, err = b.GetGateways("openvpn")
if err != nil {
t.Fatalf("getGateways openvpn returned an error: %v", err)
}
if len(gateways) != 4 {
t.Fatalf("It not right number of gateways: %v", gateways)
}
}
package bonafide
import (
"encoding/json"
"fmt"
"io"
"log"
"math/rand"
"sort"
"strconv"
"strings"
"time"
"0xacab.org/leap/bitmask-vpn/pkg/config"
)
const (
eip1API = config.APIURL + "1/config/eip-service.json"
eip3API = config.APIURL + "3/config/eip-service.json"
)
type eipService struct {
Gateways []gatewayV3
Locations map[string]location
OpenvpnConfiguration openvpnConfig `json:"openvpn_configuration"`
defaultGateway string
}
type eipServiceV1 struct {
Gateways []gatewayV1
Locations map[string]location
OpenvpnConfiguration openvpnConfig `json:"openvpn_configuration"`
}
type location struct {
CountryCode string
Hemisphere string
Name string
Timezone string
}
type gatewayV1 struct {
Capabilities struct {
Ports []string
Protocols []string
}
Host string
IPAddress string `json:"ip_address"`
Location string
}
type gatewayV3 struct {
Capabilities struct {
Transport []transportV3
}
Host string
IPAddress string `json:"ip_address"`
Location string
}
type transportV3 struct {
Type string
Protocols []string
Ports []string
Options map[string]string
}
func (b *Bonafide) fetchEipJSON() error {
resp, err := b.client.Post(eip3API, "", nil)
for err != nil {
log.Printf("Error fetching eip v3 json: %v", err)
time.Sleep(retryFetchJSONSeconds * time.Second)
resp, err = b.client.Post(eip3API, "", nil)
}
defer resp.Body.Close()
switch resp.StatusCode {
case 200:
b.eip, err = decodeEIP3(resp.Body)
case 404:
resp, err = b.client.Post(eip1API, "", nil)
if err != nil {
return err
}
defer resp.Body.Close()
b.eip, err = decodeEIP1(resp.Body)
default:
return fmt.Errorf("get eip json has failed with status: %s", resp.Status)
}
if err != nil {
return err
}
b.sortGateways()
return nil
}
func decodeEIP3(body io.Reader) (*eipService, error) {
var eip eipService
decoder := json.NewDecoder(body)
err := decoder.Decode(&eip)
return &eip, err
}
func decodeEIP1(body io.Reader) (*eipService, error) {
var eip1 eipServiceV1
decoder := json.NewDecoder(body)
err := decoder.Decode(&eip1)
if err != nil {
return nil, err
}
eip3 := eipService{
Gateways: make([]gatewayV3, len(eip1.Gateways)),
Locations: eip1.Locations,
OpenvpnConfiguration: eip1.OpenvpnConfiguration,
}
for _, g := range eip1.Gateways {
gateway := gatewayV3{
Host: g.Host,
IPAddress: g.IPAddress,
Location: g.Location,
}
gateway.Capabilities.Transport = []transportV3{
transportV3{
Type: "openvpn",
Ports: g.Capabilities.Ports,
Protocols: g.Capabilities.Protocols,
},
}
eip3.Gateways = append(eip3.Gateways, gateway)
}
return &eip3, nil
}
func (eip eipService) getGateways(transport string) []Gateway {
gws := []Gateway{}
for _, g := range eip.Gateways {
for _, t := range g.Capabilities.Transport {
if t.Type != transport {
continue
}
gateway := Gateway{
Host: g.Host,
IPAddress: g.IPAddress,
Location: g.Location,
Ports: t.Ports,
Protocols: t.Protocols,
Options: t.Options,
}
gws = append(gws, gateway)
}
}
return gws
}
func (eip *eipService) setDefaultGateway(name string) {
eip.defaultGateway = name
}
func (eip eipService) getOpenvpnArgs() []string {
args := []string{}
for arg, value := range eip.OpenvpnConfiguration {
switch v := value.(type) {
case string:
args = append(args, "--"+arg)
args = append(args, strings.Split(v, " ")...)
case bool:
if v {
args = append(args, "--"+arg)
}
default:
log.Printf("Unknown openvpn argument type: %s - %v", arg, value)
}
}
return args
}
func (eip *eipService) sortGatewaysByGeolocation(geolocatedGateways []string) {
gws := make([]gatewayV3, len(eip.Gateways))
if eip.defaultGateway != "" {
for _, gw := range eip.Gateways {
if gw.Location == eip.defaultGateway {
gws = append(gws, gw)
break
}
}
}
for _, host := range geolocatedGateways {
for _, gw := range eip.Gateways {
if gw.Host == host {
gws = append(gws, gw)
}
}
}
eip.Gateways = gws
}
type gatewayDistance struct {
gateway gatewayV3
distance int
}
func (eip *eipService) sortGatewaysByTimezone(tzOffsetHours int) {
gws := []gatewayDistance{}
for _, gw := range eip.Gateways {
distance := 13
if gw.Location == eip.defaultGateway {
distance = -1
} else {
gwOffset, err := strconv.Atoi(eip.Locations[gw.Location].Timezone)
if err != nil {
log.Printf("Error sorting gateways: %v", err)
} else {
distance = tzDistance(tzOffsetHours, gwOffset)
}
}
gws = append(gws, gatewayDistance{gw, distance})
}
rand.Seed(time.Now().UnixNano())
cmp := func(i, j int) bool {
if gws[i].distance == gws[j].distance {
return rand.Intn(2) == 1
}
return gws[i].distance < gws[j].distance
}
sort.Slice(gws, cmp)
for i, gw := range gws {
eip.Gateways[i] = gw.gateway
}
}
func tzDistance(offset1, offset2 int) int {
abs := func(x int) int {
if x < 0 {
return -x
}
return x
}
distance := abs(offset1 - offset2)
if distance > 12 {
distance = 24 - distance
}
return distance
}
{
"gateways": [
{
"capabilities": {
"adblock": false,
"filter_dns": false,
"limited": false,
"transport": [
{
"type": "openvpn",
"ports": [
"443"
],
"protocols": [
"tcp"
]
},
{
"type": "obfs4",
"ports": [
"2345"
],
"protocols": [
"tcp"
],
"options": {
"cert": "obfs-cert",
"iat-mode": "0"
}
}
],
"user_ips": false
},
"host": "1.example.com",
"ip_address": "1.1.1.1",
"location": "a"
},
{
"capabilities": {
"adblock": false,
"filter_dns": false,
"limited": false,
"transport": [
{
"type": "openvpn",
"ports": [
"443"
],
"protocols": [
"tcp"
]
}
],
"user_ips": false
},
"host": "2.example.com",
"ip_address": "2.2.2.2",
"location": "b"
},
{
"capabilities": {
"adblock": false,
"filter_dns": false,
"limited": false,
"transport": [
{
"type": "openvpn",
"ports": [
"443"
],
"protocols": [
"tcp"
]
},
{
"type": "obfs4",
"ports": [
"2345"
],
"protocols": [
"tcp"
],
"options": {
"cert": "obfs-cert",
"iat-mode": "0"
}
}
],
"user_ips": false
},
"host": "3.example.com",
"ip_address": "3.3.3.3",
"location": "b"
},
{
"capabilities": {
"adblock": false,
"filter_dns": false,
"limited": false,
"transport": [
{
"type": "openvpn",
"ports": [
"443"
],
"protocols": [
"tcp"
]
}
],
"user_ips": false
},
"host": "4.example.com",
"ip_address": "4.4.4.4",
"location": "c"
},
{
"capabilities": {
"adblock": false,
"filter_dns": false,
"limited": false,
"transport": [
{
"type": "obfs4",
"ports": [
"2345"
],
"protocols": [
"tcp"
],
"options": {
"cert": "obfs-cert",
"iat-mode": "0"
}
}
],
"user_ips": false
},
"host": "5.example.com",
"ip_address": "5.5.5.5",
"location": "c"
}
],
"locations": {
"a": {
"country_code": "AA",
"hemisphere": "N",
"name": "a",
"timezone": "-5"
},
"b": {
"country_code": "BB",
"hemisphere": "S",
"name": "b",
"timezone": "+1"
},
"c": {
"country_code": "CC",
"hemisphere": "N",
"name": "c",
"timezone": "+8"
}
},
"openvpn_configuration": {
"auth": "SHA1",
"cipher": "AES-128-CBC",
"keepalive": "10 30",
"tls-cipher": "DHE-RSA-AES128-SHA",
"tun-ipv6": true
},
"serial": 1,
"version": 3
}
......@@ -24,6 +24,7 @@ import (
"strings"
"0xacab.org/leap/bitmask-vpn/pkg/config"
"0xacab.org/leap/bitmask-vpn/pkg/standalone/bonafide"
"github.com/mitchellh/go-ps"
)
......@@ -144,7 +145,7 @@ func (l *launcher) openvpnStop() error {
return runBitmaskRoot("openvpn", "stop")
}
func (l *launcher) firewallStart(gateways []gateway) error {
func (l *launcher) firewallStart(gateways []bonafide.Gateway) error {
log.Println("firewall start")
arg := []string{"firewall", "start"}
for _, gw := range gateways {
......
......@@ -21,6 +21,7 @@ import (
"os"
"0xacab.org/leap/bitmask-vpn/pkg/config"
"0xacab.org/leap/bitmask-vpn/pkg/standalone/bonafide"
"github.com/apparentlymart/go-openvpn-mgmt/openvpn"
)
......@@ -29,7 +30,7 @@ type Bitmask struct {
tempdir string
statusCh chan string
managementClient *openvpn.MgmtClient
bonafide *bonafide
bonafide *bonafide.Bonafide
launch *launcher
}
......@@ -40,7 +41,7 @@ func Init() (*Bitmask, error) {
if err != nil {
return nil, err
}
bonafide := newBonafide()
bonafide := bonafide.New()
launch, err := newLauncher()
if err != nil {
return nil, err
......
......@@ -28,7 +28,7 @@ const (
// StartVPN for provider
func (b *Bitmask) StartVPN(provider string) error {
gateways, err := b.bonafide.getGateways()
gateways, err := b.bonafide.GetGateways("openvpn")
if err != nil {
return err
}
......@@ -42,7 +42,7 @@ func (b *Bitmask) StartVPN(provider string) error {
return err
}
arg, err := b.bonafide.getOpenvpnArgs()
arg, err := b.bonafide.GetOpenvpnArgs()
if err != nil {
return err
}
......@@ -63,7 +63,7 @@ func (b *Bitmask) getCert() (certPath string, err error) {
certPath = b.getCertPemPath()
if _, err := os.Stat(certPath); os.IsNotExist(err) {
cert, err := b.bonafide.getCertPem()
cert, err := b.bonafide.GetCertPem()
if err != nil {
return "", err
}
......@@ -95,7 +95,7 @@ func (b *Bitmask) ReloadFirewall() error {
}
if status != Off {
gateways, err := b.bonafide.getGateways()
gateways, err := b.bonafide.GetGateways("openvpn")
if err != nil {
return err
}
......@@ -129,7 +129,7 @@ func (b *Bitmask) VPNCheck() (helpers bool, priviledge bool, err error) {
// ListGateways return the names of the gateways
func (b *Bitmask) ListGateways(provider string) ([]string, error) {
gateways, err := b.bonafide.getGateways()
gateways, err := b.bonafide.GetGateways("openvpn")
if err != nil {
return nil, err
}
......@@ -142,7 +142,7 @@ func (b *Bitmask) ListGateways(provider string) ([]string, error) {
// UseGateway selects name as the default gateway
func (b *Bitmask) UseGateway(name string) error {
b.bonafide.setDefaultGateway(name)
b.bonafide.SetDefaultGateway(name)
return nil
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment