summaryrefslogtreecommitdiff
path: root/pkg/vpn
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/vpn')
-rw-r--r--pkg/vpn/bonafide/bonafide.go166
-rw-r--r--pkg/vpn/bonafide/bonafide_api_test.go15
-rw-r--r--pkg/vpn/bonafide/bonafide_integration_test.go62
-rw-r--r--pkg/vpn/bonafide/bonafide_test.go220
-rw-r--r--pkg/vpn/bonafide/eip_service.go257
-rw-r--r--pkg/vpn/bonafide/testdata/cert54
-rw-r--r--pkg/vpn/bonafide/testdata/eip-service.json113
-rw-r--r--pkg/vpn/bonafide/testdata/eip-service3.json173
-rw-r--r--pkg/vpn/launcher.go163
-rw-r--r--pkg/vpn/launcher_linux.go224
-rw-r--r--pkg/vpn/main.go86
-rw-r--r--pkg/vpn/openvpn.go255
-rw-r--r--pkg/vpn/status.go90
13 files changed, 1878 insertions, 0 deletions
diff --git a/pkg/vpn/bonafide/bonafide.go b/pkg/vpn/bonafide/bonafide.go
new file mode 100644
index 0000000..fd32f2a
--- /dev/null
+++ b/pkg/vpn/bonafide/bonafide.go
@@ -0,0 +1,166 @@
+// Copyright (C) 2018 LEAP
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// 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 bonafide
+
+import (
+ "crypto/tls"
+ "crypto/x509"
+ "encoding/json"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "time"
+
+ "0xacab.org/leap/bitmask-vpn/pkg/config"
+)
+
+const (
+ certAPI = config.APIURL + "1/cert"
+ certAPI3 = config.APIURL + "3/cert"
+ secondsPerHour = 60 * 60
+ retryFetchJSONSeconds = 15
+)
+
+type Bonafide struct {
+ client httpClient
+ eip *eipService
+ tzOffsetHours int
+}
+
+type Gateway struct {
+ Host string
+ IPAddress string
+ Location string
+ Ports []string
+ Protocols []string
+ Options map[string]string
+}
+
+type openvpnConfig map[string]interface{}
+
+type httpClient interface {
+ Post(url, contentType string, body io.Reader) (resp *http.Response, err error)
+}
+
+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 New() *Bonafide {
+ certs := x509.NewCertPool()
+ certs.AppendCertsFromPEM(config.CaCert)
+ client := &http.Client{
+ Transport: &http.Transport{
+ TLSClientConfig: &tls.Config{
+ RootCAs: certs,
+ },
+ },
+ }
+ _, tzOffsetSeconds := time.Now().Zone()
+ tzOffsetHours := tzOffsetSeconds / secondsPerHour
+
+ return &Bonafide{
+ client: client,
+ eip: nil,
+ tzOffsetHours: tzOffsetHours,
+ }
+}
+
+func (b *Bonafide) GetCertPem() ([]byte, error) {
+ resp, err := b.client.Post(certAPI, "", nil)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode == 404 {
+ resp, err = b.client.Post(certAPI3, "", nil)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ }
+ if resp.StatusCode != 200 {
+ return nil, fmt.Errorf("get vpn cert has failed with status: %s", resp.Status)
+ }
+
+ return ioutil.ReadAll(resp.Body)
+}
+
+func (b *Bonafide) GetGateways(transport string) ([]Gateway, error) {
+ if b.eip == nil {
+ err := b.fetchEipJSON()
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return b.eip.getGateways(transport), nil
+}
+
+func (b *Bonafide) SetDefaultGateway(name string) {
+ b.eip.setDefaultGateway(name)
+ b.sortGateways()
+}
+
+func (b *Bonafide) GetOpenvpnArgs() ([]string, error) {
+ if b.eip == nil {
+ err := b.fetchEipJSON()
+ if err != nil {
+ return nil, err
+ }
+ }
+ return b.eip.getOpenvpnArgs(), nil
+}
+
+func (b *Bonafide) fetchGeolocation() ([]string, error) {
+ resp, err := b.client.Post(config.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) sortGateways() {
+ geolocatedGateways, _ := b.fetchGeolocation()
+
+ if len(geolocatedGateways) > 0 {
+ b.eip.sortGatewaysByGeolocation(geolocatedGateways)
+ } else {
+ log.Printf("Falling back to timezone heuristic for gateway selection")
+ b.eip.sortGatewaysByTimezone(b.tzOffsetHours)
+ }
+}
diff --git a/pkg/vpn/bonafide/bonafide_api_test.go b/pkg/vpn/bonafide/bonafide_api_test.go
new file mode 100644
index 0000000..7b48d8f
--- /dev/null
+++ b/pkg/vpn/bonafide/bonafide_api_test.go
@@ -0,0 +1,15 @@
+package bonafide
+
+import (
+ "log"
+ "testing"
+)
+
+func TestBonafideAPI(t *testing.T) {
+ b := New()
+ cert, err := b.GetCertPem()
+ if err != nil {
+ log.Fatal(err)
+ }
+ log.Println(string(cert))
+}
diff --git a/pkg/vpn/bonafide/bonafide_integration_test.go b/pkg/vpn/bonafide/bonafide_integration_test.go
new file mode 100644
index 0000000..bea00fe
--- /dev/null
+++ b/pkg/vpn/bonafide/bonafide_integration_test.go
@@ -0,0 +1,62 @@
+// +build integration
+// Copyright (C) 2018 LEAP
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// 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 bonafide
+
+import (
+ "bytes"
+ "testing"
+)
+
+const (
+ gwIP = "199.58.81.145"
+)
+
+var (
+ privateKeyHeader = []byte("-----BEGIN RSA PRIVATE KEY-----")
+ certHeader = []byte("-----BEGIN CERTIFICATE-----")
+)
+
+func TestIntegrationGetCert(t *testing.T) {
+ b := New()
+ cert, err := b.GetCertPem()
+ if err != nil {
+ t.Fatal("getCert returned an error: ", err)
+ }
+
+ if !bytes.Contains(cert, privateKeyHeader) {
+ t.Errorf("No private key present: \n%q", cert)
+ }
+
+ if !bytes.Contains(cert, certHeader) {
+ t.Errorf("No cert present: \n%q", cert)
+ }
+}
+
+func TestGetGateways(t *testing.T) {
+ b := New()
+ gateways, err := b.GetGateways("openvpn")
+ if err != nil {
+ t.Fatal("getGateways returned an error: ", err)
+ }
+
+ for _, gw := range gateways {
+ if gw.IPAddress == gwIP {
+ return
+ }
+ }
+ t.Errorf("%s not in the list", gwIP)
+}
diff --git a/pkg/vpn/bonafide/bonafide_test.go b/pkg/vpn/bonafide/bonafide_test.go
new file mode 100644
index 0000000..8fb7f72
--- /dev/null
+++ b/pkg/vpn/bonafide/bonafide_test.go
@@ -0,0 +1,220 @@
+// Copyright (C) 2018 LEAP
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// 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 bonafide
+
+import (
+ "io"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "reflect"
+ "strconv"
+ "strings"
+ "testing"
+)
+
+const (
+ certPath = "testdata/cert"
+ eip1Path = "testdata/eip-service.json"
+ eipPath = "testdata/eip-service3.json"
+)
+
+type client struct {
+ path string
+}
+
+func (c client) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) {
+ f, err := os.Open(c.path)
+ return &http.Response{
+ Body: f,
+ StatusCode: 200,
+ }, err
+}
+
+func TestGetCert(t *testing.T) {
+ b := Bonafide{client: client{certPath}}
+ cert, err := b.GetCertPem()
+ if err != nil {
+ t.Fatal("getCert returned an error: ", err)
+ }
+
+ f, err := os.Open(certPath)
+ if err != nil {
+ t.Fatal("Can't open ", certPath, ": ", err)
+ }
+ defer f.Close()
+
+ certData, err := ioutil.ReadAll(f)
+ if err != nil {
+ t.Fatal("Can't read all: ", err)
+ }
+ if !reflect.DeepEqual(certData, cert) {
+ t.Errorf("cert doesn't match")
+ }
+}
+
+func TestGatewayTzLocation(t *testing.T) {
+ // tzOffset -> location
+ values := map[int]string{
+ -12: "c",
+ -10: "a",
+ -5: "a",
+ -3: "a",
+ -1: "b",
+ 0: "b",
+ 2: "b",
+ 5: "c",
+ 8: "c",
+ 12: "c",
+ }
+
+ for tzOffset, location := range values {
+ b := Bonafide{
+ client: client{eipPath},
+ tzOffsetHours: tzOffset,
+ }
+ gateways, err := b.GetGateways("openvpn")
+ if err != nil {
+ t.Errorf("getGateways returned an error: %v", err)
+ continue
+ }
+ if len(gateways) < 4 {
+ t.Errorf("Wrong number of gateways: %d", len(gateways))
+ continue
+
+ }
+ if gateways[0].Location != location {
+ t.Errorf("Wrong location for tz %d: %s, expected: %s", tzOffset, gateways[0].Location, location)
+ }
+ }
+}
+
+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)
+ }
+}
diff --git a/pkg/vpn/bonafide/eip_service.go b/pkg/vpn/bonafide/eip_service.go
new file mode 100644
index 0000000..c097e8a
--- /dev/null
+++ b/pkg/vpn/bonafide/eip_service.go
@@ -0,0 +1,257 @@
+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:
+ buf := make([]byte, 128)
+ resp.Body.Read(buf)
+ log.Printf("Error fetching eip v3 json: %s", buf)
+ resp, err = b.client.Post(eip1API, "", nil)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != 200 {
+ return fmt.Errorf("get eip json has failed with status: %s", resp.Status)
+ }
+
+ 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 {
+ log.Printf("Error fetching eip v1 json: %v", err)
+ 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
+}
diff --git a/pkg/vpn/bonafide/testdata/cert b/pkg/vpn/bonafide/testdata/cert
new file mode 100644
index 0000000..4968b3f
--- /dev/null
+++ b/pkg/vpn/bonafide/testdata/cert
@@ -0,0 +1,54 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpQIBAAKCAQEA3TQAmZs9U6e1xqQqVWkb132AGXdaO97bsXPOrrKUp63hKeXD
+2OQbmG96H3COi0uubVFQT1cAmpuym2COtgahQlnv42p0u2CYsBqfrCHw3iSK7nf6
+Q8RaG2oUIvlQj5m4DUk1wfRBgG5z0pN2HwFWmgheoT0RnOelTO3vcLCaSJA6PF4M
+Wehg5ScXi9wr0vibKsANpqab3oUxcHEYcNcKfJKRnXryJx6ctLrRp1WPv3JAXLnn
+oUtQ00S0dSHrLED5yPwGFV08q4bkv54qFai2cPO8ITReC6BpvrilBzOjT6fjCmzm
+6MCwBot7aRHYcWJgfp7H2b2S7T2qhnC4c2u6mwIDAQABAoIBAFue83SsOS2SNJdv
+Xd18qLyLzeg+aFCOET8h8YSokSwWuEGLWqBWcxujaNjm3RPTKA89c9848RYY0VTM
+HLBGdLqv183BRVJrQzMGBAbfFA5e4nC9nxo8lPnv6SFHVNf12qceILcSPaM9nJmm
+3HEhM8afGtr8GXR8+hmwH9H0RCMzXIjO1//WrY3nfOP8LRuQpSnnhsfZRyngWhot
+xsJlDP5plFNw7J/PDLtbjnbOXOktv0fhhq7aWR+A+0s0627r7Tidk1YoNwusYJeB
+uKrzNW1+c7Z9xl2yvMQ6+0Wry7A5YUVYP/BakYb/f/skB4ox/zz8vcWeQ4ShZ7m6
+LwPN0ckCgYEA+9wjSBbOlksath3QrJywikD1sQYFOdNrINTBFQnuipKOYKLJNhQM
+OzKHY1OiO7G6FcEvz9gKYMpMyMOs8TsISyKPNOXLpnwpgIUx6WRo6qxgEuyWLpBb
+Q3Kodl1a/q51dw56pPDEATKjSB1CjXXzm717m5FimH5csPKj9SzrGecCgYEA4Nbb
+QML1Jh9cu7TvlK3WqbAFJa4Mx/0+OQ+5xlhbs/ygn3/AZiSsPWdNK11XJ25jgGJw
+AucXr/kHgwJX23kFpCYB3zZE0Vh/hOqk/KlUFmptuADIDOAVst0v8MqBLZpZessN
+TXph5VBT6P51Oz/ZLC67uno02R1vUhDMB5VCyy0CgYEAoriZuuuxUXz4pw0gU0Vw
+8gICOvsuySuFHVMX5GXkTnddsaW65kuRk3WT72KLgJHVLlUAdQKZwesyLMvvonOH
+ajPL3ltRdiDmF3j2xFnxRx1TfSaJ6U+vBya/HKo4Li+9CMy8BHDh0fxLbj4pT4gT
+el2zzNDjqK6LaG976t24j6UCgYEAyTD5uRW7cIWX4Y+i4xQ7hlQwBuucHEkMKNtd
+jZL7XC+vO4qBi+U9CyUo9KjtmCc7emKbgL1xgNICWsT6ATZmSeCIxEg3hG0Ajtu5
+Dy4mRHiv/XsViA/s2sT6ZSmQNlJrx2lzWeUtPJmIvHEWThJwLw0Sh2dbavzf5DuL
+ly2FO3ECgYEA5AKnPQo45I+abEA/zKHsKHCqBPEbIaZpJJTaiInrsLiGJf+WzN4N
+zr/VAzvY+v0X5RgZmROY5ZLPVf2fTeVNzU5WzoB78hHOI67YI2Sbq7jZlatOgX4z
+Ur2BQdT0bW6VINYpDLUvS4goW5p0nQbGItdk69yyef1v3NDbCJ/Sg+Q=
+-----END RSA PRIVATE KEY-----
+-----BEGIN CERTIFICATE-----
+MIIEnDCCAoSgAwIBAgIRAJzr5xxPLgDD+drzU+5p8cgwDQYJKoZIhvcNAQELBQAw
+dTEYMBYGA1UECgwPUmlzZXVwIE5ldHdvcmtzMRswGQYDVQQLDBJodHRwczovL3Jp
+c2V1cC5uZXQxPDA6BgNVBAMMM1Jpc2V1cCBOZXR3b3JrcyBSb290IENBIChjbGll
+bnQgY2VydGlmaWNhdGVzIG9ubHkhKTAeFw0xODEwMjMwMDAwMDBaFw0xOTAxMjMw
+MDAwMDBaMC0xKzApBgNVBAMMIlVOTElNSVRFRDVmbzdsMmxiY3g0OWR5ZzR5MWY4
+YXN3YXcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDdNACZmz1Tp7XG
+pCpVaRvXfYAZd1o73tuxc86uspSnreEp5cPY5BuYb3ofcI6LS65tUVBPVwCam7Kb
+YI62BqFCWe/janS7YJiwGp+sIfDeJIrud/pDxFobahQi+VCPmbgNSTXB9EGAbnPS
+k3YfAVaaCF6hPRGc56VM7e9wsJpIkDo8XgxZ6GDlJxeL3CvS+JsqwA2mppvehTFw
+cRhw1wp8kpGdevInHpy0utGnVY+/ckBcueehS1DTRLR1IessQPnI/AYVXTyrhuS/
+nioVqLZw87whNF4LoGm+uKUHM6NPp+MKbObowLAGi3tpEdhxYmB+nsfZvZLtPaqG
+cLhza7qbAgMBAAGjbzBtMB0GA1UdDgQWBBRwXpI96PjilFPrkK+CHUPia++ISTAL
+BgNVHQ8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwIwCQYDVR0TBAIwADAfBgNV
+HSMEGDAWgBQX9BvV5SoBAU1rol02CikJlmWARjANBgkqhkiG9w0BAQsFAAOCAgEA
+ryNFLixuicVRepocY2lTSY0cpG0eRmLuYJGupk9KeiLA5YEFzl4ZfXJLi+9UHoUR
+Bgfe6QYLBb77nO24CoeiMJQw6s593ctMLiMU++fjew31gNp6aA9DmvbLd+fNuLyO
+XObRtGw99M37cyf3ZS2SEbTBr4NBp/r3OCyUYsxPYKOzEkr9kNYa8ZZSI960i7/R
+/aiq2qemQaOQHTlmrhcBuARJoRVVlLnn2zgLSVm6ptbFLNtAk0lWriUT/WlRmn8j
+Cyn/JOuo/1wtrK1dHkaXr8bkEq1oFQzcwMN85hrZKWU0BCehELZtiUg8grqaX/sf
+/jaXD61FEqWjIXeGqY/K6ruosZCw2R8sQYzTuQNHMjxmx+J3pch7dMmJPbmA3HW2
+nA7yVp51SX8iZ26zb40S7GG6RNesU+BZxz05XVLt1GwyLx/uNxS4rFpKAT/+ifWG
+3Y1j1lMqBxx6RbuqiM1TWqU7Xtzu3hf8ytP5qP7kudXn1TyNtpZCIrzbTXbLnYiD
+nH4ZQEWGyAKBOz41eOcG6EXn0TznSGE593ueXBeFnsym7i9MjoOWNGaJ7UbkipfX
+FzxirlY5IRkWnmHCL0wGUg6YGnZ1OQ8VBBGb/dBPRMDwA7zWvoM7+3yDLR3aRaLH
+mTQzNzu3jy6CRdlpIUcPRcgbniySip1jJrHRYBui+9w=
+-----END CERTIFICATE-----
diff --git a/pkg/vpn/bonafide/testdata/eip-service.json b/pkg/vpn/bonafide/testdata/eip-service.json
new file mode 100644
index 0000000..d5f2413
--- /dev/null
+++ b/pkg/vpn/bonafide/testdata/eip-service.json
@@ -0,0 +1,113 @@
+{
+ "gateways": [
+ {
+ "capabilities": {
+ "adblock": false,
+ "filter_dns": false,
+ "limited": false,
+ "ports": [
+ "443"
+ ],
+ "protocols": [
+ "tcp"
+ ],
+ "transport": [
+ "openvpn"
+ ],
+ "user_ips": false
+ },
+ "host": "1.example.com",
+ "ip_address": "1.1.1.1",
+ "location": "a"
+ },
+ {
+ "capabilities": {
+ "adblock": false,
+ "filter_dns": false,
+ "limited": false,
+ "ports": [
+ "443"
+ ],
+ "protocols": [
+ "tcp"
+ ],
+ "transport": [
+ "openvpn"
+ ],
+ "user_ips": false
+ },
+ "host": "2.example.com",
+ "ip_address": "2.2.2.2",
+ "location": "b"
+ },
+ {
+ "capabilities": {
+ "adblock": false,
+ "filter_dns": false,
+ "limited": false,
+ "ports": [
+ "443"
+ ],
+ "protocols": [
+ "tcp"
+ ],
+ "transport": [
+ "openvpn"
+ ],
+ "user_ips": false
+ },
+ "host": "3.example.com",
+ "ip_address": "3.3.3.3",
+ "location": "b"
+ },
+ {
+ "capabilities": {
+ "adblock": false,
+ "filter_dns": false,
+ "limited": false,
+ "ports": [
+ "443"
+ ],
+ "protocols": [
+ "tcp"
+ ],
+ "transport": [
+ "openvpn"
+ ],
+ "user_ips": false
+ },
+ "host": "4.example.com",
+ "ip_address": "4.4.4.4",
+ "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": 1
+}
diff --git a/pkg/vpn/bonafide/testdata/eip-service3.json b/pkg/vpn/bonafide/testdata/eip-service3.json
new file mode 100644
index 0000000..cefd6c0
--- /dev/null
+++ b/pkg/vpn/bonafide/testdata/eip-service3.json
@@ -0,0 +1,173 @@
+{
+ "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
+}
diff --git a/pkg/vpn/launcher.go b/pkg/vpn/launcher.go
new file mode 100644
index 0000000..e18fdc6
--- /dev/null
+++ b/pkg/vpn/launcher.go
@@ -0,0 +1,163 @@
+// +build !linux
+// Copyright (C) 2018 LEAP
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// 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 vpn
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "strconv"
+ "strings"
+ "time"
+
+ "0xacab.org/leap/bitmask-vpn/pkg/config"
+ "0xacab.org/leap/bitmask-vpn/pkg/vpn/bonafide"
+)
+
+type launcher struct {
+ helperAddr string
+}
+
+const initialHelperPort = 7171
+
+func probeHelperPort(port int) int {
+ // this should be enough for a local reply
+ timeout := time.Duration(500 * time.Millisecond)
+ c := http.Client{Timeout: timeout}
+ for {
+ if smellsLikeOurHelperSpirit(port, &c) {
+ return port
+ }
+ port++
+ if port > 65535 {
+ break
+ }
+ }
+ return 0
+}
+
+func smellsLikeOurHelperSpirit(port int, c *http.Client) bool {
+ uri := "http://localhost:" + strconv.Itoa(port) + "/version"
+ log.Println("probing for helper at", uri)
+ resp, err := c.Get(uri)
+ if err != nil {
+ return false
+ }
+ if resp.StatusCode == 200 {
+ ver, err := ioutil.ReadAll(resp.Body)
+ defer resp.Body.Close()
+ if err != nil {
+ return false
+ }
+ if strings.Contains(string(ver), config.ApplicationName) {
+ return true
+ } else {
+ log.Println("Another helper replied to our version request:", string(ver))
+ }
+ }
+ return false
+}
+
+func newLauncher() (*launcher, error) {
+ helperPort := probeHelperPort(initialHelperPort)
+ helperAddr := "http://localhost:" + strconv.Itoa(helperPort)
+ return &launcher{helperAddr}, nil
+}
+
+func (l *launcher) close() error {
+ return nil
+}
+
+func (l *launcher) check() (helpers bool, priviledge bool, err error) {
+ return true, true, nil
+}
+
+func (l *launcher) openvpnStart(flags ...string) error {
+ byteFlags, err := json.Marshal(flags)
+ if err != nil {
+ return err
+ }
+ return l.send("/openvpn/start", byteFlags)
+}
+
+func (l *launcher) openvpnStop() error {
+ return l.send("/openvpn/stop", nil)
+}
+
+func (l *launcher) firewallStart(gateways []bonafide.Gateway) error {
+ ipList := make([]string, len(gateways))
+ for i, gw := range gateways {
+ ipList[i] = gw.IPAddress
+ }
+ byteIPs, err := json.Marshal(ipList)
+ if err != nil {
+ return err
+ }
+ return l.send("/firewall/start", byteIPs)
+}
+
+func (l *launcher) firewallStop() error {
+ return l.send("/firewall/stop", nil)
+}
+
+func (l *launcher) firewallIsUp() bool {
+ var isup bool = false
+ res, err := http.Post(l.helperAddr+"/firewall/isup", "", nil)
+ if err != nil {
+ return false
+ }
+ defer res.Body.Close()
+
+ if res.StatusCode != http.StatusOK {
+ fmt.Printf("Got an error status code for firewall/isup: %v\n", res.StatusCode)
+ isup = false
+ } else {
+ upStr, err := ioutil.ReadAll(res.Body)
+ if err != nil {
+ fmt.Errorf("Error getting body for firewall/isup: %q", err)
+ return false
+ }
+ isup, err = strconv.ParseBool(string(upStr))
+ if err != nil {
+ fmt.Errorf("Error parsing body for firewall/isup: %q", err)
+ return false
+ }
+ }
+ return isup
+}
+
+func (l *launcher) send(path string, body []byte) error {
+ var reader io.Reader
+ if body != nil {
+ reader = bytes.NewReader(body)
+ }
+ res, err := http.Post(l.helperAddr+path, "", reader)
+ if err != nil {
+ return err
+ }
+ defer res.Body.Close()
+
+ resErr, err := ioutil.ReadAll(res.Body)
+ if len(resErr) > 0 {
+ return fmt.Errorf("Helper returned an error: %q", resErr)
+ }
+ return err
+}
diff --git a/pkg/vpn/launcher_linux.go b/pkg/vpn/launcher_linux.go
new file mode 100644
index 0000000..71a74ea
--- /dev/null
+++ b/pkg/vpn/launcher_linux.go
@@ -0,0 +1,224 @@
+// +build linux
+// Copyright (C) 2018 LEAP
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// 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 vpn
+
+import (
+ "errors"
+ "log"
+ "os"
+ "os/exec"
+ "strings"
+
+ "0xacab.org/leap/bitmask-vpn/pkg/config"
+ "0xacab.org/leap/bitmask-vpn/pkg/vpn/bonafide"
+ "github.com/keybase/go-ps"
+)
+
+const (
+ systemOpenvpnPath = "/usr/sbin/openvpn"
+ snapOpenvpnPath = "/snap/bin/" + config.BinaryName + ".openvpn"
+ snapBitmaskRootPath = "/snap/bin/" + config.BinaryName + ".bitmask-root"
+)
+
+var bitmaskRootPaths = []string{
+ "/usr/sbin/bitmask-root",
+ "/usr/local/sbin/bitmask-root",
+}
+
+type launcher struct {
+ openvpnCh chan []string
+}
+
+func newLauncher() (*launcher, error) {
+ l := launcher{make(chan []string, 1)}
+ go l.openvpnRunner()
+ return &l, nil
+}
+
+func (l *launcher) close() error {
+ return nil
+}
+
+func (l *launcher) check() (helpers bool, priviledge bool, err error) {
+
+ /*
+ isRunning, err := isPolkitRunning()
+ if err != nil {
+ return
+ }
+ if !isRunning {
+ polkitPath := getPolkitPath()
+ if polkitPath == "" {
+ return true, false, nil
+ }
+ cmd := exec.Command("setsid", polkitPath)
+ err = cmd.Start()
+ if err != nil {
+ return
+ }
+ isRunning, err = isPolkitRunning()
+ return true, isRunning, err
+ }
+ */
+
+ return true, true, nil
+}
+
+func isPolkitRunning() (bool, error) {
+ var polkitProcNames = [...]string{
+ "polkit-gnome-authentication-agent-1",
+ "polkit-kde-authentication-agent-1",
+ "polkit-mate-authentication-agent-1",
+ "lxpolkit",
+ "lxsession",
+ "gnome-shell",
+ "gnome-flashback",
+ "fingerprint-polkit-agent",
+ "xfce-polkit",
+ }
+
+ processes, err := ps.Processes()
+ if err != nil {
+ return false, err
+ }
+
+ for _, proc := range processes {
+ executable := proc.Executable()
+ for _, name := range polkitProcNames {
+ if strings.Contains(executable, name) {
+ return true, nil
+ }
+ }
+ }
+ return false, nil
+}
+
+func getPolkitPath() string {
+ var polkitPaths = [...]string{
+ "/usr/bin/lxpolkit",
+ "/usr/bin/lxqt-policykit-agent",
+ "/usr/lib/policykit-1-gnome/polkit-gnome-authentication-agent-1",
+ "/usr/lib/x86_64-linux-gnu/polkit-mate/polkit-mate-authentication-agent-1",
+ "/usr/lib/mate-polkit/polkit-mate-authentication-agent-1",
+ "/usr/lib/x86_64-linux-gnu/libexec/polkit-kde-authentication-agent-1",
+ "/usr/lib/kde4/libexec/polkit-kde-authentication-agent-1",
+ // now we get weird
+ "/usr/libexec/policykit-1-pantheon/pantheon-agent-polkit",
+ "/usr/lib/polkit-1-dde/dde-polkit-agent",
+ // do you know some we"re still missing? :)
+ }
+
+ for _, polkit := range polkitPaths {
+ _, err := os.Stat(polkit)
+ if err == nil {
+ return polkit
+ }
+ }
+ return ""
+}
+
+func (l *launcher) openvpnStart(flags ...string) error {
+ log.Println("openvpn start: ", flags)
+ arg := []string{"openvpn", "start", getOpenvpnPath()}
+ arg = append(arg, flags...)
+ l.openvpnCh <- arg
+ return nil
+}
+
+func (l *launcher) openvpnStop() error {
+ l.openvpnCh <- nil
+ log.Println("openvpn stop")
+ return runBitmaskRoot("openvpn", "stop")
+}
+
+func (l *launcher) firewallStart(gateways []bonafide.Gateway) error {
+ log.Println("firewall start")
+ arg := []string{"firewall", "start"}
+ for _, gw := range gateways {
+ arg = append(arg, gw.IPAddress)
+ }
+ return runBitmaskRoot(arg...)
+}
+
+func (l *launcher) firewallStop() error {
+ log.Println("firewall stop")
+ return runBitmaskRoot("firewall", "stop")
+}
+
+func (l *launcher) firewallIsUp() bool {
+ err := runBitmaskRoot("firewall", "isup")
+ return err == nil
+}
+
+func (l *launcher) openvpnRunner(arg ...string) {
+ running := false
+ runOpenvpn := func(arg []string) {
+ for running {
+ err := runBitmaskRoot(arg...)
+ if err != nil {
+ log.Printf("An error ocurred running openvpn: %v", err)
+ }
+ }
+ }
+
+ for arg := range l.openvpnCh {
+ if arg == nil {
+ running = false
+ } else {
+ running = true
+ go runOpenvpn(arg)
+ }
+ }
+}
+
+func runBitmaskRoot(arg ...string) error {
+ bitmaskRoot, err := bitmaskRootPath()
+ if err != nil {
+ return err
+ }
+ arg = append([]string{bitmaskRoot}, arg...)
+
+ out, err := exec.Command("pkexec", arg...).Output()
+ if err != nil && arg[2] != "isup" {
+ log.Println("Error while running bitmask-root:")
+ log.Println("args: ", arg)
+ log.Println("output: ", string(out))
+ }
+ return err
+}
+
+func bitmaskRootPath() (string, error) {
+ if os.Getenv("SNAP") != "" {
+ path := snapBitmaskRootPath
+ if _, err := os.Stat(path); !os.IsNotExist(err) {
+ return path, nil
+ }
+ }
+ for _, path := range bitmaskRootPaths {
+ if _, err := os.Stat(path); !os.IsNotExist(err) {
+ return path, nil
+ }
+ }
+ return "", errors.New("No bitmask-root found")
+}
+
+func getOpenvpnPath() string {
+ if os.Getenv("SNAP") != "" {
+ return snapOpenvpnPath
+ }
+ return systemOpenvpnPath
+}
diff --git a/pkg/vpn/main.go b/pkg/vpn/main.go
new file mode 100644
index 0000000..ce599c9
--- /dev/null
+++ b/pkg/vpn/main.go
@@ -0,0 +1,86 @@
+// Copyright (C) 2018-2020 LEAP
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// 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 vpn
+
+import (
+ "io/ioutil"
+ "log"
+ "os"
+
+ "0xacab.org/leap/bitmask-vpn/pkg/config"
+ "0xacab.org/leap/bitmask-vpn/pkg/vpn/bonafide"
+ "0xacab.org/leap/shapeshifter"
+ "github.com/apparentlymart/go-openvpn-mgmt/openvpn"
+)
+
+// Bitmask holds the bitmask client data
+type Bitmask struct {
+ tempdir string
+ statusCh chan string
+ managementClient *openvpn.MgmtClient
+ bonafide *bonafide.Bonafide
+ launch *launcher
+ transport string
+ shapes *shapeshifter.ShapeShifter
+}
+
+// Init the connection to bitmask
+func Init() (*Bitmask, error) {
+ statusCh := make(chan string, 10)
+ tempdir, err := ioutil.TempDir("", "leap-")
+ if err != nil {
+ return nil, err
+ }
+ bonafide := bonafide.New()
+ launch, err := newLauncher()
+ if err != nil {
+ return nil, err
+ }
+ b := Bitmask{tempdir, statusCh, nil, bonafide, launch, "", nil}
+
+ err = b.StopVPN()
+ if err != nil {
+ return nil, err
+ }
+ err = ioutil.WriteFile(b.getCaCertPath(), config.CaCert, 0600)
+
+ go b.openvpnManagement()
+ return &b, err
+}
+
+// GetStatusCh returns a channel that will recieve VPN status changes
+func (b *Bitmask) GetStatusCh() <-chan string {
+ return b.statusCh
+}
+
+// Close the connection to bitmask
+func (b *Bitmask) Close() {
+ log.Printf("Close: cleanup and vpn shutdown...")
+ b.StopVPN()
+ err := b.launch.close()
+ if err != nil {
+ log.Printf("There was an error closing the launcher: %v", err)
+ }
+ err = os.RemoveAll(b.tempdir)
+ if err != nil {
+ log.Printf("There was an error removing temp dir: %v", err)
+ }
+}
+
+// Version gets the bitmask version string
+func (b *Bitmask) Version() (string, error) {
+ return "", nil
+}
diff --git a/pkg/vpn/openvpn.go b/pkg/vpn/openvpn.go
new file mode 100644
index 0000000..a75b830
--- /dev/null
+++ b/pkg/vpn/openvpn.go
@@ -0,0 +1,255 @@
+// Copyright (C) 2018-2020 LEAP
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// 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 vpn
+
+import (
+ "fmt"
+ "io/ioutil"
+ "log"
+ "os"
+ "path"
+ "strconv"
+ "strings"
+
+ "0xacab.org/leap/shapeshifter"
+)
+
+const (
+ openvpnManagementAddr = "127.0.0.1"
+ openvpnManagementPort = "6061"
+)
+
+// StartVPN for provider
+func (b *Bitmask) StartVPN(provider string) error {
+ var proxy string
+ if b.transport != "" {
+ var err error
+ proxy, err = b.startTransport()
+ if err != nil {
+ return err
+ }
+ }
+
+ return b.startOpenVPN(proxy)
+}
+
+func (b *Bitmask) startTransport() (proxy string, err error) {
+ proxy = "127.0.0.1:4430"
+ if b.shapes != nil {
+ return proxy, nil
+ }
+
+ gateways, err := b.bonafide.GetGateways(b.transport)
+ if err != nil {
+ return "", err
+ }
+ if len(gateways) == 0 {
+ log.Printf("No gateway for transport %s in provider", b.transport)
+ return "", nil
+ }
+
+ for _, gw := range gateways {
+ if _, ok := gw.Options["cert"]; !ok {
+ continue
+ }
+ b.shapes = &shapeshifter.ShapeShifter{
+ Cert: gw.Options["cert"],
+ Target: gw.IPAddress + ":" + gw.Ports[0],
+ SocksAddr: proxy,
+ }
+ go b.listenShapeErr()
+ if iatMode, ok := gw.Options["iat-mode"]; ok {
+ b.shapes.IatMode, err = strconv.Atoi(iatMode)
+ if err != nil {
+ b.shapes.IatMode = 0
+ }
+ }
+ err = b.shapes.Open()
+ if err != nil {
+ log.Printf("Can't connect to transport %s: %v", b.transport, err)
+ continue
+ }
+ return proxy, nil
+ }
+ return "", fmt.Errorf("No working gateway for transport %s: %v", b.transport, err)
+}
+
+func (b *Bitmask) listenShapeErr() {
+ ch := b.shapes.GetErrorChannel()
+ for {
+ err, more := <-ch
+ if !more {
+ return
+ }
+ log.Printf("Error from shappeshifter: %v", err)
+ }
+}
+
+func (b *Bitmask) startOpenVPN(proxy string) error {
+ certPemPath, err := b.getCert()
+ if err != nil {
+ return err
+ }
+ arg, err := b.bonafide.GetOpenvpnArgs()
+ if err != nil {
+ return err
+ }
+
+ if proxy == "" {
+ gateways, err := b.bonafide.GetGateways("openvpn")
+ if err != nil {
+ return err
+ }
+ err = b.launch.firewallStart(gateways)
+ if err != nil {
+ return err
+ }
+
+ for _, gw := range gateways {
+ for _, port := range gw.Ports {
+ arg = append(arg, "--remote", gw.IPAddress, port, "tcp4")
+ }
+ }
+ } else {
+ gateways, err := b.bonafide.GetGateways(b.transport)
+ if err != nil {
+ return err
+ }
+ err = b.launch.firewallStart(gateways)
+ if err != nil {
+ return err
+ }
+
+ proxyArgs := strings.Split(proxy, ":")
+ arg = append(arg, "--remote", proxyArgs[0], proxyArgs[1], "tcp4")
+ }
+ arg = append(arg,
+ "--verb", "1",
+ "--management-client",
+ "--management", openvpnManagementAddr, openvpnManagementPort,
+ "--ca", b.getCaCertPath(),
+ "--cert", certPemPath,
+ "--key", certPemPath)
+ return b.launch.openvpnStart(arg...)
+}
+
+func (b *Bitmask) getCert() (certPath string, err error) {
+ certPath = b.getCertPemPath()
+
+ if _, err := os.Stat(certPath); os.IsNotExist(err) {
+ cert, err := b.bonafide.GetCertPem()
+ if err != nil {
+ return "", err
+ }
+ err = ioutil.WriteFile(certPath, cert, 0600)
+ }
+
+ return certPath, err
+}
+
+// StopVPN or cancel
+func (b *Bitmask) StopVPN() error {
+ err := b.launch.firewallStop()
+ if err != nil {
+ return err
+ }
+ if b.shapes != nil {
+ b.shapes.Close()
+ b.shapes = nil
+ }
+ return b.launch.openvpnStop()
+}
+
+// ReloadFirewall restarts the firewall
+func (b *Bitmask) ReloadFirewall() error {
+ err := b.launch.firewallStop()
+ if err != nil {
+ return err
+ }
+
+ status, err := b.GetStatus()
+ if err != nil {
+ return err
+ }
+
+ if status != Off {
+ gateways, err := b.bonafide.GetGateways("openvpn")
+ if err != nil {
+ return err
+ }
+ return b.launch.firewallStart(gateways)
+ }
+ return nil
+}
+
+// GetStatus returns the VPN status
+func (b *Bitmask) GetStatus() (string, error) {
+ status, err := b.getOpenvpnState()
+ if err != nil {
+ status = Off
+ }
+ if status == Off && b.launch.firewallIsUp() {
+ return Failed, nil
+ }
+ return status, nil
+}
+
+// InstallHelpers into the system
+func (b *Bitmask) InstallHelpers() error {
+ // TODO
+ return nil
+}
+
+// VPNCheck returns if the helpers are installed and up to date and if polkit is running
+func (b *Bitmask) VPNCheck() (helpers bool, priviledge bool, err error) {
+ return b.launch.check()
+}
+
+// ListGateways return the names of the gateways
+func (b *Bitmask) ListGateways(provider string) ([]string, error) {
+ gateways, err := b.bonafide.GetGateways("openvpn")
+ if err != nil {
+ return nil, err
+ }
+ gatewayNames := make([]string, len(gateways))
+ for i, gw := range gateways {
+ gatewayNames[i] = gw.Location
+ }
+ return gatewayNames, nil
+}
+
+// UseGateway selects name as the default gateway
+func (b *Bitmask) UseGateway(name string) error {
+ b.bonafide.SetDefaultGateway(name)
+ return nil
+}
+
+// UseTransport selects an obfuscation transport to use
+func (b *Bitmask) UseTransport(transport string) error {
+ if transport != "obfs4" {
+ return fmt.Errorf("Transport %s not implemented", transport)
+ }
+ b.transport = transport
+ return nil
+}
+
+func (b *Bitmask) getCertPemPath() string {
+ return path.Join(b.tempdir, "openvpn.pem")
+}
+
+func (b *Bitmask) getCaCertPath() string {
+ return path.Join(b.tempdir, "cacert.pem")
+}
diff --git a/pkg/vpn/status.go b/pkg/vpn/status.go
new file mode 100644
index 0000000..a4d7ada
--- /dev/null
+++ b/pkg/vpn/status.go
@@ -0,0 +1,90 @@
+// Copyright (C) 2018-2020 LEAP
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// 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 vpn
+
+import (
+ "fmt"
+ "log"
+
+ "github.com/apparentlymart/go-openvpn-mgmt/openvpn"
+)
+
+const (
+ On = "on"
+ Off = "off"
+ Starting = "starting"
+ Stopping = "stopping"
+ Failed = "failed"
+)
+
+var statusNames = map[string]string{
+ "CONNECTING": Starting,
+ "WAIT": Starting,
+ "AUTH": Starting,
+ "GET_CONFIG": Starting,
+ "ASSIGN_IP": Starting,
+ "ADD_ROUTES": Starting,
+ "CONNECTED": On,
+ "RECONNECTING": Starting,
+ "EXITING": Stopping,
+ "OFF": Off,
+ "FAILED": Off,
+}
+
+func (b *Bitmask) openvpnManagement() {
+ // TODO: we should warn the user on ListenAndServe errors
+ newConnection := func(conn openvpn.IncomingConn) {
+ eventCh := make(chan openvpn.Event, 10)
+ log.Println("New connection into the management")
+ b.managementClient = conn.Open(eventCh)
+ b.managementClient.SetStateEvents(true)
+ b.eventHandler(eventCh)
+ }
+ log.Fatal(openvpn.ListenAndServe(
+ fmt.Sprintf("%s:%s", openvpnManagementAddr, openvpnManagementPort),
+ openvpn.IncomingConnHandlerFunc(newConnection),
+ ))
+}
+
+func (b *Bitmask) eventHandler(eventCh <-chan openvpn.Event) {
+ for event := range eventCh {
+ log.Printf("Event: %v", event)
+ stateEvent, ok := event.(*openvpn.StateEvent)
+ if !ok {
+ continue
+ }
+ status, ok := statusNames[stateEvent.NewState()]
+ if ok {
+ b.statusCh <- status
+ }
+ }
+ b.statusCh <- Off
+}
+
+func (b *Bitmask) getOpenvpnState() (string, error) {
+ if b.managementClient == nil {
+ return "", fmt.Errorf("No management connected")
+ }
+ stateEvent, err := b.managementClient.LatestState()
+ if err != nil {
+ return "", err
+ }
+ status, ok := statusNames[stateEvent.NewState()]
+ if !ok {
+ return "", fmt.Errorf("Unkonw status")
+ }
+ return status, nil
+}