aboutsummaryrefslogtreecommitdiffstats
path: root/cmd/ircmirror/vpnprovider.go
blob: 634a3d5b113f29bcb352572a73e676b94a553aa2 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
/* SPDX-License-Identifier: GPL-2.0
 *
 * Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved.
 */

package main

import (
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/url"
	"regexp"
	"strings"

	"golang.org/x/crypto/curve25519"
	"inet.af/netaddr"
)

const vpnProviderMaxEndpointsInParallel = 20

type vpnEndpoint struct {
	publicKey []byte
	endpoint  netaddr.IPPort
	name      string
	country   string
}

type vpnConf struct {
	privateKey []byte
	ips        []netaddr.IP
	dnses      []netaddr.IP
}

type apiRelaysWireGuardV1Key []byte

func (key *apiRelaysWireGuardV1Key) UnmarshalText(text []byte) error {
	k, err := base64.StdEncoding.DecodeString(string(text))
	if err != nil {
		return err
	}
	if len(k) != 32 {
		return errors.New("key must be 32 bytes")
	}
	*key = k
	return nil
}

type apiRelaysWireGuardV1Relay struct {
	Hostname     string                  `json:"hostname"`
	EndpointV4   netaddr.IP              `json:"ipv4_addr_in"`
	EndpointV6   netaddr.IP              `json:"ipv6_addr_in"`
	PublicKey    apiRelaysWireGuardV1Key `json:"public_key"`
	MultihopPort uint16                  `json:"multihop_port"`
}

type apiRelaysWireGuardV1City struct {
	Name      string                      `json:"name"`
	Code      string                      `json:"code"`
	Latitude  float64                     `json:"latitude"`
	Longitude float64                     `json:"Longitude"`
	Relays    []apiRelaysWireGuardV1Relay `json:"relays"`
}

type apiRelaysWireGuardV1Country struct {
	Name   string                     `json:"name"`
	Code   string                     `json:"code"`
	Cities []apiRelaysWireGuardV1City `json:"cities"`
}
type apiRelaysWireGuardV1Root struct {
	Countries []apiRelaysWireGuardV1Country `json:"countries"`
}

func getVpnEndpoints() ([]vpnEndpoint, error) {
	log.Println("Getting VPN server list")
	resp, err := http.Get("https://api.mullvad.net/public/relays/wireguard/v1/")
	if err != nil {
		return nil, err
	}
	bytes, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	var relays apiRelaysWireGuardV1Root
	err = json.Unmarshal(bytes, &relays)
	if err != nil {
		return nil, err
	}
	var endpoints []vpnEndpoint
	for _, country := range relays.Countries {
		for _, city := range country.Cities {
			for _, relay := range city.Relays {
				endpoints = append(endpoints, vpnEndpoint{
					publicKey: relay.PublicKey,
					endpoint:  netaddr.IPPortFrom(relay.EndpointV4, 51820),
					name:      strings.TrimSuffix(relay.Hostname, "-wireguard"),
				})
			}
		}
	}
	return endpoints, nil
}

func registerVpnConf(privateKey []byte, accountId string) (*vpnConf, error) {
	log.Println("Registering VPN private key")
	publicKey, err := curve25519.X25519(privateKey, curve25519.Basepoint)
	if err != nil {
		return nil, err
	}
	resp, err := http.PostForm("https://api.mullvad.net/wg/", url.Values{
		"account": {accountId},
		"pubkey":  {base64.StdEncoding.EncodeToString(publicKey)},
	})
	if err != nil {
		return nil, err
	}
	ipBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	if match, _ := regexp.Match(`^[0-9a-f:/.,]+$`, ipBytes); !match {
		return nil, fmt.Errorf("registration rejected: %q", string(ipBytes))
	}
	conf := &vpnConf{privateKey: privateKey, dnses: []netaddr.IP{netaddr.MustParseIP("193.138.218.74")}}
	for _, ipStr := range strings.Split(string(ipBytes), ",") {
		ip, err := netaddr.ParseIPPrefix(strings.TrimSpace(ipStr))
		if err != nil {
			return nil, err
		}
		conf.ips = append(conf.ips, ip.IP())
	}
	return conf, nil
}