Skip to content
Snippets Groups Projects
Commit e733205a authored by ale's avatar ale
Browse files

Add OpenVPN protocol support

parent 9fd88bb1
No related branches found
No related tags found
1 merge request!6Add OpenVPN protocol support
......@@ -352,6 +352,66 @@ application).
If you don't need the CSS-selector features, you can also just use the
http probe to make simple requests with the *open* step.
## openvpn_ping
The *openvpn_ping* prober attempts to connect to an openvpn server, and after
performing a successful handshake it will attempt to send and receive ICMP Echo
packets through the tunnel to verify that the gateway can route traffic.
The probe configuration requires the following *params*. Most of them belong to the subset of `openvpn` configuration options understood by the minimal implementation of the protocol we use:
* *pingTarget*, the IP that should be pinged through the VPN gateway
* *remote*, the IP of the OpenVPN remote,
* *proto*, the protocol in use (either *tcp* or *udp*)
* *port*, the port the OpenVPN server is listening on
* *cipher*, the preferred `--data-cipher` for the VPN data channel.
* *auth*, the `HMAC` algorithm used to authenticate the packets in the data channel. This is ignored if an AEAD cipher is picked.
* *cert*, the path to a valid certificate that the client will use to athenticate against the service.
* *key*, the path to a valid key that the client will use to authenticate against the service
* *ca*, the path to the certificate authority that the client will use to authenticate the server.
As an example, we can then combine the variable expansion capabilities to define a parametrized probe that will check several remotes on different ports and protocols:
```json
{
"vars": {
"remotes": [
{
"ip": "10.10.10.10",
"proto": "tcp",
"port": "1194"
},
{
"ip": "11.11.11.11",
"proto": "udp",
"port": "1194"
}
]
},
"probes": [
{
"type": "openvpn_ping",
"name": "openvpn_ping/${remotes.ip}:${remotes.port}/${remotes.proto}",
"loop": [
"remotes"
],
"params": {
"pingTarget": "1.1.1.1",
"remote": "${remotes.ip}",
"proto": "${remotes.proto}",
"port": "${remotes.port}",
"cipher": "AES-256-GCM",
"auth": "SHA1",
"cert": "/path/to/cert.pem",
"key": "/path/to/cert.pem",
"ca": "/path/to/ca.crt"
}
}
]
}
```
# Common configuration features
## DNS overrides
......
......@@ -18,6 +18,7 @@ import (
_ "git.autistici.org/ai3/tools/service-prober/probes/http"
_ "git.autistici.org/ai3/tools/service-prober/probes/imap"
_ "git.autistici.org/ai3/tools/service-prober/probes/openvpn"
)
var (
......
......@@ -2,7 +2,11 @@ module git.autistici.org/ai3/tools/service-prober
go 1.15
// temporary replacement until ongoing MR is merged upstream
replace github.com/ooni/minivpn v0.0.4 => github.com/ainghazal/minivpn v0.0.4-0.20220623143937-59ea5af1507c
require (
github.com/PuerkitoBio/goquery v1.8.0
github.com/ooni/minivpn v0.0.4
github.com/prometheus/client_golang v1.12.2
)
This diff is collapsed.
package openvpn
import (
"context"
"encoding/json"
"errors"
"log"
"os"
"time"
"git.autistici.org/ai3/tools/service-prober/common/vars"
"git.autistici.org/ai3/tools/service-prober/probes"
"git.autistici.org/ai3/tools/service-prober/protocol/openvpn"
"github.com/ooni/minivpn/vpn"
)
const (
pingCount = 5
extraTimeoutSeconds = 10
)
func timeoutSecondsFromCount(count int) time.Duration {
waitOnLastOne := time.Duration(5) * time.Second
return time.Duration(count)*time.Second + waitOnLastOne
}
type openVPNProbeSpec struct {
Remote string `json:"remote"`
Proto string `json:"proto"`
Port string `json:"port"`
Cipher string `json:"cipher"`
Auth string `json:"auth"`
PingTarget string `json:"pingTarget"`
Cert string `json:"cert"`
Key string `json:"key"`
Ca string `json:"ca"`
}
func parseOpenVPNProbeSpec(params json.RawMessage) (probes.Spec, error) {
var spec openVPNProbeSpec
err := json.Unmarshal(params, &spec)
return &spec, err
}
func (spec *openVPNProbeSpec) Build(lookup map[string]interface{}) (probes.ProbeImpl, error) {
expanded, err := vars.Expand(spec, lookup)
if err != nil {
return nil, err
}
s := expanded.(*openVPNProbeSpec)
// Sanity checks.
if s.Remote == "" {
return nil, errors.New("remote is unset")
}
if s.Cipher == "" {
return nil, errors.New("cipher is unset")
}
if s.Auth == "" {
return nil, errors.New("auth is unset")
}
if s.Cert == "" {
return nil, errors.New("cert is unset")
}
if s.Key == "" {
return nil, errors.New("key is unset")
}
if s.Ca == "" {
return nil, errors.New("ca is unset")
}
if ok, _ := fileExists(s.Cert); !ok {
return nil, errors.New("cert path not found")
}
if ok, _ := fileExists(s.Key); !ok {
return nil, errors.New("key path not found")
}
if ok, _ := fileExists(s.Ca); !ok {
return nil, errors.New("ca path not found")
}
return &openVPNProbe{
pingTarget: s.PingTarget,
remote: s.Remote,
proto: s.Proto,
port: s.Port,
cipher: s.Cipher,
auth: s.Auth,
key: s.Key,
cert: s.Cert,
ca: s.Ca,
}, nil
}
type openVPNProbe struct {
pingTarget string
remote string
proto string
port string
cipher string
auth string
cert string
key string
ca string
}
func (p *openVPNProbe) RunProbe(ctx context.Context, debug *log.Logger) error {
debug.Printf("making OpenVPN connection to %s (%s/%s)", p.remote, p.port, p.proto)
var proto int
switch p.proto {
case "udp":
proto = vpn.UDPMode
case "tcp":
proto = vpn.TCPMode
}
opts := &vpn.Options{
Auth: p.auth,
Cipher: p.cipher,
Port: p.port,
Remote: p.remote,
Proto: proto,
Cert: p.cert,
Key: p.key,
Ca: p.ca,
}
log.Println("Options:", opts)
timeout := timeoutSecondsFromCount(pingCount) + extraTimeoutSeconds
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
pingOpts := &openvpn.PingOptions{
Target: p.pingTarget,
Count: pingCount,
Timeout: timeout,
}
pinger, err := openvpn.NewPingerFromOptions(opts, ctx, pingOpts)
if err != nil {
return err
}
err = pinger.Run()
if err != nil {
return err
}
log.Printf("loss: %d%%\n", pinger.PacketLoss())
if pinger.PacketLoss() > 50 {
return errors.New("packet loss too high")
}
debug.Printf("success")
return nil
}
func init() {
probes.RegisterProbeType("openvpn_ping", parseOpenVPNProbeSpec)
}
func fileExists(name string) (bool, error) {
_, err := os.Stat(name)
if err == nil {
return true, nil
}
if errors.Is(err, os.ErrNotExist) {
return false, nil
}
return false, err
}
package openvpn
import (
"context"
"time"
"github.com/ooni/minivpn/extras/ping"
"github.com/ooni/minivpn/vpn"
)
type PingOptions struct {
Target string
Count int
Timeout time.Duration
}
// OpenVPNPinger sends and receives ICMP Echo packets over an OpenVPN connection. Returns a configured Pinger, and an error if the Pinger object cannot be properly initialized.
func NewPingerFromOptions(opts *vpn.Options, ctx context.Context, pingOpts *PingOptions) (*ping.Pinger, error) {
rawDialer := vpn.NewRawDialerWithContext(opts, ctx)
conn, err := rawDialer.Dial()
if err != nil {
return nil, err
}
pinger := ping.New(pingOpts.Target, conn)
pinger.Count = pingOpts.Count
pinger.Timeout = pingOpts.Timeout
return pinger, nil
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment