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

Merge branch 'openvpn' into 'master'

Add OpenVPN protocol support

See merge request !6
parents 9fd88bb1 5b75f470
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 !fileExists(s.Cert) {
return nil, errors.New("cert path not found")
}
if !fileExists(s.Key) {
return nil, errors.New("key path not found")
}
if !fileExists(s.Ca) {
return nil, errors.New("ca path not found")
}
var vpnProto int
switch s.Proto {
case "udp":
vpnProto = vpn.UDPMode
case "tcp":
vpnProto = vpn.TCPMode
default:
return nil, errors.New("unknown proto")
}
return &openVPNProbe{
pingTarget: s.PingTarget,
remote: s.Remote,
proto: s.Proto,
vpnProto: vpnProto,
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
vpnProto int
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)
opts := &vpn.Options{
Auth: p.auth,
Cipher: p.cipher,
Port: p.port,
Remote: p.remote,
Proto: p.vpnProto,
Cert: p.cert,
Key: p.key,
Ca: p.ca,
}
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
}
debug.Printf("loss: %d%%", 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 {
_, err := os.Stat(name)
return err == nil
}
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