New package: minichecker
This commit is contained in:
parent
5d8bcd55ce
commit
ae3b2e6f3b
23
pkg/minichecker/Dockerfile
Normal file
23
pkg/minichecker/Dockerfile
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
FROM golang:alpine as gobuild
|
||||||
|
|
||||||
|
ENV GOOS linux
|
||||||
|
ENV GOARCH amd64
|
||||||
|
|
||||||
|
RUN apk add --no-cache git gcc
|
||||||
|
|
||||||
|
WORKDIR /go/src/minichecker
|
||||||
|
|
||||||
|
ADD cmd ./
|
||||||
|
|
||||||
|
RUN GO111MODULE=off go get -d -v
|
||||||
|
RUN GO111MODULE=off go build -v -tags netgo
|
||||||
|
|
||||||
|
|
||||||
|
FROM alpine
|
||||||
|
MAINTAINER Pierre-Olivier Mercier <nemunaire@nemunai.re>
|
||||||
|
|
||||||
|
COPY --from=gobuild /go/src/minichecker/minichecker /bin/minichecker
|
||||||
|
|
||||||
|
RUN mkdir /etc/wireguard && touch /etc/wireguard/.wireguard
|
||||||
|
|
||||||
|
ENTRYPOINT ["/bin/minichecker"]
|
2
pkg/minichecker/build.yml
Normal file
2
pkg/minichecker/build.yml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
image: minichecker
|
||||||
|
network: true
|
12
pkg/minichecker/cmd/adlin.conf
Normal file
12
pkg/minichecker/cmd/adlin.conf
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
[Interface]
|
||||||
|
PrivateKey = AKZx4pYq2Do2j/OOFFJ6eA/O7DiAkr6aINJRHdMnkVU=
|
||||||
|
[Peer]
|
||||||
|
PublicKey = uSpqyYovvP4OG6wDxZ0Qkq45MfyK58PMUuPaLesY8FI=
|
||||||
|
Endpoint = 82.64.31.248:42912
|
||||||
|
AllowedIPs = ::/0
|
||||||
|
PersistentKeepalive = 5
|
||||||
|
# MyIPv6=2a01:e0a:2b:2252:1::2a/64
|
||||||
|
# MyNetwork=2a01:e0a:2b:2252:1::/80
|
||||||
|
# GWIPv6=2a01:e0a:2b:2252::1
|
||||||
|
# MyLogin=nemunaire
|
||||||
|
# KeySign=lcUxw+Gw+m9q21yvAL5BJJ9xpACTefeyR6qBsBFPyO9lqtrq/qqWAvf/gITtXdNmRne4FU72pw9pzL2J8SVeAw==
|
1
pkg/minichecker/cmd/adlin.token
Normal file
1
pkg/minichecker/cmd/adlin.token
Normal file
@ -0,0 +1 @@
|
|||||||
|
QMzkCN3PPg
|
169
pkg/minichecker/cmd/checker.go
Normal file
169
pkg/minichecker/cmd/checker.go
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"github.com/sparrc/go-ping"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DEFAULT_RESOLVER = "2a01:e0a:2b:2250::1"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
verbose = false
|
||||||
|
test_name = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
// ICMP
|
||||||
|
|
||||||
|
func check_ping(ip string, cb func(pkt *ping.Packet)) (err error) {
|
||||||
|
var pinger *ping.Pinger
|
||||||
|
pinger, err = ping.NewPinger(ip)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer pinger.Stop()
|
||||||
|
|
||||||
|
pinger.Timeout = time.Second * 5
|
||||||
|
pinger.Count = 1
|
||||||
|
pinger.OnRecv = cb
|
||||||
|
pinger.SetPrivileged(true)
|
||||||
|
pinger.Run()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func check_dns(domain, ip string) (aaaa net.IP, err error) {
|
||||||
|
client := dns.Client{Timeout: time.Second * 5}
|
||||||
|
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetQuestion(domain, dns.TypeAAAA)
|
||||||
|
|
||||||
|
var r *dns.Msg
|
||||||
|
r, _, err = client.Exchange(m, fmt.Sprintf("[%s]:53", ip))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r == nil {
|
||||||
|
err = errors.New("response is nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Rcode != dns.RcodeSuccess {
|
||||||
|
err = errors.New("failed to get a valid answer")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, answer := range r.Answer {
|
||||||
|
if t, ok := answer.(*dns.AAAA); ok {
|
||||||
|
aaaa = t.AAAA
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// PORT 80
|
||||||
|
|
||||||
|
func check_http(ip, dn string) (err error) {
|
||||||
|
client := &http.Client{
|
||||||
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||||
|
return http.ErrUseLastResponse
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
req, errr := http.NewRequest("GET", fmt.Sprintf("http://[%s]/", ip), nil)
|
||||||
|
if errr != nil {
|
||||||
|
return errr
|
||||||
|
}
|
||||||
|
|
||||||
|
if dn != "" {
|
||||||
|
req.Header.Add("Host", strings.TrimSuffix(dn, "."))
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp *http.Response
|
||||||
|
resp, err = client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if dn != "" && resp.StatusCode >= 400 {
|
||||||
|
return fmt.Errorf("Bad status, got: %d (%s)", resp.StatusCode, resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = ioutil.ReadAll(resp.Body)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// PORT 443
|
||||||
|
|
||||||
|
func check_https(domain, ip string) (err error) {
|
||||||
|
var resp *http.Response
|
||||||
|
resp, err = http.Get(fmt.Sprintf("https://%s/", strings.TrimSuffix(domain, ".")))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 300 && resp.StatusCode < 400 {
|
||||||
|
loc := resp.Header.Get("Location")
|
||||||
|
if loc != "" && strings.HasSuffix(dns.Fqdn(loc), domain) {
|
||||||
|
if dns.Fqdn(loc) == domain {
|
||||||
|
return fmt.Errorf("Redirection loop %s redirect to %s", domain, loc)
|
||||||
|
} else if err = check_https(dns.Fqdn(loc), ip); err != nil {
|
||||||
|
return fmt.Errorf("Error after following redirection to %s: %w", loc, err)
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode >= 300 {
|
||||||
|
return fmt.Errorf("Bad status, got: %d (%s)", resp.StatusCode, resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = ioutil.ReadAll(resp.Body)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func wksChecker() {
|
||||||
|
if collectorpubkey == nil {
|
||||||
|
var err error
|
||||||
|
if collectorpubkey, err = getCollectorPublicKey(); err != nil {
|
||||||
|
log.Println("Contact collector:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := encodeData(SendMeta{Login: Login}, map[string]*string{
|
||||||
|
test_name: nil,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Unable to encode message:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = sendData(data)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Unable to send message:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
v, _ := json.Marshal(data)
|
||||||
|
if verbose {
|
||||||
|
log.Printf("sent %s", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
83
pkg/minichecker/cmd/encode.go
Normal file
83
pkg/minichecker/cmd/encode.go
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
Login string
|
||||||
|
KeySign string
|
||||||
|
target string
|
||||||
|
)
|
||||||
|
|
||||||
|
func getCollectorPublicKey() (key []byte, err error) {
|
||||||
|
var resp *http.Response
|
||||||
|
resp, err = http.Get(target + "/api/collector_info")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
rd := base64.NewDecoder(base64.StdEncoding, json.NewDecoder(resp.Body).Buffered())
|
||||||
|
return ioutil.ReadAll(rd)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SendMeta struct {
|
||||||
|
Time time.Time `json:"time"`
|
||||||
|
Login string `json:"login"`
|
||||||
|
Test string `json:"test,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SendContent struct {
|
||||||
|
Meta []byte `json:"meta"`
|
||||||
|
Data []byte `json:"data"`
|
||||||
|
Sign []byte `json:"sign"`
|
||||||
|
Key []byte `json:"key"`
|
||||||
|
KeySign string `json:"keysign"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeData(meta SendMeta, v interface{}) (data SendContent, err error) {
|
||||||
|
if meta.Time.IsZero() {
|
||||||
|
meta.Time = time.Now()
|
||||||
|
}
|
||||||
|
var b []byte
|
||||||
|
b, err = json.Marshal(meta)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data.Meta = []byte(base64.StdEncoding.EncodeToString(b))
|
||||||
|
|
||||||
|
b, err = json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data.Data = []byte(base64.StdEncoding.EncodeToString(b))
|
||||||
|
|
||||||
|
data.Sign = ed25519.Sign(myprivkey, append(data.Meta, data.Data...))
|
||||||
|
data.Key = myprivkey.Public().(ed25519.PublicKey)
|
||||||
|
data.KeySign = KeySign
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendData(data SendContent) (err error) {
|
||||||
|
b := new(bytes.Buffer)
|
||||||
|
json.NewEncoder(b).Encode(data)
|
||||||
|
|
||||||
|
var resp *http.Response
|
||||||
|
resp, err = http.Post(target+"/remote", "application/json", b)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
1
pkg/minichecker/cmd/go.mod
Normal file
1
pkg/minichecker/cmd/go.mod
Normal file
@ -0,0 +1 @@
|
|||||||
|
module example.com/mod
|
152
pkg/minichecker/cmd/main.go
Normal file
152
pkg/minichecker/cmd/main.go
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/sha512"
|
||||||
|
"flag"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gopkg.in/fsnotify.v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
myprivkey ed25519.PrivateKey
|
||||||
|
collectorpubkey ed25519.PublicKey
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var tokenfile = flag.String("token-file", "/etc/wireguard/adlin.token", "Path to your token file")
|
||||||
|
var wgfile = flag.String("wg-file", "/etc/wireguard/adlin.conf", "Path to your wireguard configuration file")
|
||||||
|
var interval = flag.Duration("check-interval", 30*time.Second, "Interval between two checks")
|
||||||
|
flag.StringVar(&test_name, "test-name", "", "Name of the test to report")
|
||||||
|
flag.StringVar(&target, "target", "https://adlin.nemunai.re", "HTTP server to contact")
|
||||||
|
flag.BoolVar(&verbose, "verbose", verbose, "Enable verbose mode")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if test_name == "" {
|
||||||
|
test_name, _ = os.Hostname()
|
||||||
|
}
|
||||||
|
|
||||||
|
// First load token if it exists
|
||||||
|
if _, err := os.Stat(*tokenfile); !os.IsNotExist(err) {
|
||||||
|
if err := loadToken(*tokenfile); err != nil {
|
||||||
|
log.Fatal("ERROR: Unable to read token file:", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Fatal("Unable to find token file:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load of configuration if it exists
|
||||||
|
if _, err := os.Stat(*wgfile); !os.IsNotExist(err) {
|
||||||
|
if err := loadSettings(*wgfile); err != nil {
|
||||||
|
log.Println("ERROR: Unable to read wg settings:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register SIGHUP
|
||||||
|
c := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(c, syscall.SIGHUP)
|
||||||
|
go func() {
|
||||||
|
for range c {
|
||||||
|
log.Println("SIGHUP received, reloading settings...")
|
||||||
|
if err := loadSettings(*wgfile); err != nil {
|
||||||
|
log.Println("ERROR: Unable to read wg settings:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Watch the configuration file
|
||||||
|
if watcher, err := fsnotify.NewWatcher(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
} else {
|
||||||
|
if err := watcher.Add(path.Dir(*wgfile)); err != nil {
|
||||||
|
log.Fatal("Unable to watch: ", path.Dir(*wgfile), ": ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer watcher.Close()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case ev := <-watcher.Events:
|
||||||
|
if path.Base(ev.Name) == *wgfile && ev.Op&(fsnotify.Write|fsnotify.Create) != 0 {
|
||||||
|
log.Println("Settings file changes, reloading it!")
|
||||||
|
if err := loadSettings(*wgfile); err != nil {
|
||||||
|
log.Println("ERROR: Unable to read wg settings:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case err := <-watcher.Errors:
|
||||||
|
log.Println("watcher error:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare graceful shutdown
|
||||||
|
interrupt := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM)
|
||||||
|
|
||||||
|
// Delay first check randomly
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
n := rand.Intn(10)
|
||||||
|
time.Sleep(time.Duration(n) * time.Second)
|
||||||
|
|
||||||
|
ticker := time.NewTicker(*interval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
// Launch checker
|
||||||
|
//wksChecker()
|
||||||
|
loop:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-interrupt:
|
||||||
|
break loop
|
||||||
|
case <-ticker.C:
|
||||||
|
wksChecker()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadToken(filename string) error {
|
||||||
|
if fd, err := os.Open(filename); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
defer fd.Close()
|
||||||
|
|
||||||
|
if b, err := ioutil.ReadAll(fd); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
seed := sha512.Sum512(bytes.TrimSpace(b))
|
||||||
|
myprivkey = ed25519.NewKeyFromSeed(seed[:ed25519.SeedSize])
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadSettings(filename string) error {
|
||||||
|
if fd, err := os.Open(filename); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
defer fd.Close()
|
||||||
|
|
||||||
|
if b, err := ioutil.ReadAll(fd); err != nil {
|
||||||
|
return err
|
||||||
|
} else if wgConf, err := FromWgQuick(string(b), "wg0"); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
Login = wgConf.Interface.MyLogin
|
||||||
|
KeySign = wgConf.Interface.KeySign
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
409
pkg/minichecker/cmd/wg.go
Normal file
409
pkg/minichecker/cmd/wg.go
Normal file
@ -0,0 +1,409 @@
|
|||||||
|
/* SPDX-License-Identifier: MIT
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* From https://git.zx2c4.com/wireguard-windows/conf/
|
||||||
|
*/
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/subtle"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const KeyLength = 32
|
||||||
|
|
||||||
|
type IPCidr struct {
|
||||||
|
IP net.IP
|
||||||
|
Cidr uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
type Endpoint struct {
|
||||||
|
Host string
|
||||||
|
Port uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
type Key [KeyLength]byte
|
||||||
|
type HandshakeTime time.Duration
|
||||||
|
type Bytes uint64
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Name string
|
||||||
|
Interface Interface
|
||||||
|
Peers []Peer
|
||||||
|
}
|
||||||
|
|
||||||
|
type Interface struct {
|
||||||
|
PrivateKey Key
|
||||||
|
MyLogin string
|
||||||
|
KeySign string
|
||||||
|
Addresses []IPCidr
|
||||||
|
ListenPort uint16
|
||||||
|
MTU uint16
|
||||||
|
DNS []net.IP
|
||||||
|
DNSSearch []string
|
||||||
|
PreUp string
|
||||||
|
PostUp string
|
||||||
|
PreDown string
|
||||||
|
PostDown string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Peer struct {
|
||||||
|
PublicKey Key
|
||||||
|
PresharedKey Key
|
||||||
|
AllowedIPs []IPCidr
|
||||||
|
Endpoint Endpoint
|
||||||
|
PersistentKeepalive uint16
|
||||||
|
|
||||||
|
RxBytes Bytes
|
||||||
|
TxBytes Bytes
|
||||||
|
LastHandshakeTime HandshakeTime
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Key) IsZero() bool {
|
||||||
|
var zeros Key
|
||||||
|
return subtle.ConstantTimeCompare(zeros[:], k[:]) == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
type ParseError struct {
|
||||||
|
why string
|
||||||
|
offender string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ParseError) Error() string {
|
||||||
|
return fmt.Sprintf("%s: %q", e.why, e.offender)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseIPCidr(s string) (ipcidr *IPCidr, err error) {
|
||||||
|
var addrStr, cidrStr string
|
||||||
|
var cidr int
|
||||||
|
|
||||||
|
i := strings.IndexByte(s, '/')
|
||||||
|
if i < 0 {
|
||||||
|
addrStr = s
|
||||||
|
} else {
|
||||||
|
addrStr, cidrStr = s[:i], s[i+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
err = &ParseError{fmt.Sprintf("Invalid IP address"), s}
|
||||||
|
addr := net.ParseIP(addrStr)
|
||||||
|
if addr == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
maybeV4 := addr.To4()
|
||||||
|
if maybeV4 != nil {
|
||||||
|
addr = maybeV4
|
||||||
|
}
|
||||||
|
if len(cidrStr) > 0 {
|
||||||
|
err = &ParseError{fmt.Sprintf("Invalid network prefix length"), s}
|
||||||
|
cidr, err = strconv.Atoi(cidrStr)
|
||||||
|
if err != nil || cidr < 0 || cidr > 128 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if cidr > 32 && maybeV4 != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if maybeV4 != nil {
|
||||||
|
cidr = 32
|
||||||
|
} else {
|
||||||
|
cidr = 128
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &IPCidr{addr, uint8(cidr)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseEndpoint(s string) (*Endpoint, error) {
|
||||||
|
i := strings.LastIndexByte(s, ':')
|
||||||
|
if i < 0 {
|
||||||
|
return nil, &ParseError{fmt.Sprintf("Missing port from endpoint"), s}
|
||||||
|
}
|
||||||
|
host, portStr := s[:i], s[i+1:]
|
||||||
|
if len(host) < 1 {
|
||||||
|
return nil, &ParseError{fmt.Sprintf("Invalid endpoint host"), host}
|
||||||
|
}
|
||||||
|
port, err := parsePort(portStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
hostColon := strings.IndexByte(host, ':')
|
||||||
|
if host[0] == '[' || host[len(host)-1] == ']' || hostColon > 0 {
|
||||||
|
err := &ParseError{fmt.Sprintf("Brackets must contain an IPv6 address"), host}
|
||||||
|
if len(host) > 3 && host[0] == '[' && host[len(host)-1] == ']' && hostColon > 0 {
|
||||||
|
end := len(host) - 1
|
||||||
|
if i := strings.LastIndexByte(host, '%'); i > 1 {
|
||||||
|
end = i
|
||||||
|
}
|
||||||
|
maybeV6 := net.ParseIP(host[1:end])
|
||||||
|
if maybeV6 == nil || len(maybeV6) != net.IPv6len {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
host = host[1 : len(host)-1]
|
||||||
|
}
|
||||||
|
return &Endpoint{host, uint16(port)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseMTU(s string) (uint16, error) {
|
||||||
|
m, err := strconv.Atoi(s)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if m < 576 || m > 65535 {
|
||||||
|
return 0, &ParseError{fmt.Sprintf("Invalid MTU"), s}
|
||||||
|
}
|
||||||
|
return uint16(m), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePort(s string) (uint16, error) {
|
||||||
|
m, err := strconv.Atoi(s)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if m < 0 || m > 65535 {
|
||||||
|
return 0, &ParseError{fmt.Sprintf("Invalid port"), s}
|
||||||
|
}
|
||||||
|
return uint16(m), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePersistentKeepalive(s string) (uint16, error) {
|
||||||
|
if s == "off" {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
m, err := strconv.Atoi(s)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if m < 0 || m > 65535 {
|
||||||
|
return 0, &ParseError{fmt.Sprintf("Invalid persistent keepalive"), s}
|
||||||
|
}
|
||||||
|
return uint16(m), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseKeyBase64(s string) (*Key, error) {
|
||||||
|
k, err := base64.StdEncoding.DecodeString(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &ParseError{fmt.Sprintf("Invalid key: %v", err), s}
|
||||||
|
}
|
||||||
|
if len(k) != KeyLength {
|
||||||
|
return nil, &ParseError{fmt.Sprintf("Keys must decode to exactly 32 bytes"), s}
|
||||||
|
}
|
||||||
|
var key Key
|
||||||
|
copy(key[:], k)
|
||||||
|
return &key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseKeyHex(s string) (*Key, error) {
|
||||||
|
k, err := hex.DecodeString(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &ParseError{fmt.Sprintf("Invalid key: %v", err), s}
|
||||||
|
}
|
||||||
|
if len(k) != KeyLength {
|
||||||
|
return nil, &ParseError{fmt.Sprintf("Keys must decode to exactly 32 bytes"), s}
|
||||||
|
}
|
||||||
|
var key Key
|
||||||
|
copy(key[:], k)
|
||||||
|
return &key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseBytesOrStamp(s string) (uint64, error) {
|
||||||
|
b, err := strconv.ParseUint(s, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, &ParseError{fmt.Sprintf("Number must be a number between 0 and 2^64-1: %v", err), s}
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitList(s string) ([]string, error) {
|
||||||
|
var out []string
|
||||||
|
for _, split := range strings.Split(s, ",") {
|
||||||
|
trim := strings.TrimSpace(split)
|
||||||
|
if len(trim) == 0 {
|
||||||
|
return nil, &ParseError{fmt.Sprintf("Two commas in a row"), s}
|
||||||
|
}
|
||||||
|
out = append(out, trim)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type parserState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
inInterfaceSection parserState = iota
|
||||||
|
inPeerSection
|
||||||
|
notInASection
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Config) maybeAddPeer(p *Peer) {
|
||||||
|
if p != nil {
|
||||||
|
c.Peers = append(c.Peers, *p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromWgQuick(s string, name string) (*Config, error) {
|
||||||
|
lines := strings.Split(s, "\n")
|
||||||
|
parserState := notInASection
|
||||||
|
conf := Config{Name: name}
|
||||||
|
sawPrivateKey := false
|
||||||
|
var peer *Peer
|
||||||
|
for _, line := range lines {
|
||||||
|
pound := strings.IndexByte(line, '#')
|
||||||
|
if pound >= 0 {
|
||||||
|
if !strings.Contains(line, "MyLogin") && !strings.Contains(line, "KeySign") {
|
||||||
|
line = line[:pound]
|
||||||
|
} else {
|
||||||
|
line = line[pound+1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
lineLower := strings.ToLower(line)
|
||||||
|
if len(line) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if lineLower == "[interface]" {
|
||||||
|
conf.maybeAddPeer(peer)
|
||||||
|
parserState = inInterfaceSection
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if lineLower == "[peer]" {
|
||||||
|
conf.maybeAddPeer(peer)
|
||||||
|
peer = &Peer{}
|
||||||
|
parserState = inPeerSection
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if parserState == notInASection {
|
||||||
|
return nil, &ParseError{fmt.Sprintf("Line must occur in a section"), line}
|
||||||
|
}
|
||||||
|
equals := strings.IndexByte(line, '=')
|
||||||
|
if equals < 0 {
|
||||||
|
return nil, &ParseError{fmt.Sprintf("Config key is missing an equals separator"), line}
|
||||||
|
}
|
||||||
|
key, val := strings.TrimSpace(lineLower[:equals]), strings.TrimSpace(line[equals+1:])
|
||||||
|
if len(val) == 0 {
|
||||||
|
return nil, &ParseError{fmt.Sprintf("Key must have a value"), line}
|
||||||
|
}
|
||||||
|
if parserState == inInterfaceSection {
|
||||||
|
switch key {
|
||||||
|
case "privatekey":
|
||||||
|
k, err := parseKeyBase64(val)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
conf.Interface.PrivateKey = *k
|
||||||
|
sawPrivateKey = true
|
||||||
|
case "listenport":
|
||||||
|
p, err := parsePort(val)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
conf.Interface.ListenPort = p
|
||||||
|
case "mtu":
|
||||||
|
m, err := parseMTU(val)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
conf.Interface.MTU = m
|
||||||
|
case "address":
|
||||||
|
addresses, err := splitList(val)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, address := range addresses {
|
||||||
|
a, err := parseIPCidr(address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
conf.Interface.Addresses = append(conf.Interface.Addresses, *a)
|
||||||
|
}
|
||||||
|
case "dns":
|
||||||
|
addresses, err := splitList(val)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, address := range addresses {
|
||||||
|
a := net.ParseIP(address)
|
||||||
|
if a == nil {
|
||||||
|
conf.Interface.DNSSearch = append(conf.Interface.DNSSearch, address)
|
||||||
|
} else {
|
||||||
|
conf.Interface.DNS = append(conf.Interface.DNS, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "preup":
|
||||||
|
conf.Interface.PreUp = val
|
||||||
|
case "postup":
|
||||||
|
conf.Interface.PostUp = val
|
||||||
|
case "predown":
|
||||||
|
conf.Interface.PreDown = val
|
||||||
|
case "postdown":
|
||||||
|
conf.Interface.PostDown = val
|
||||||
|
default:
|
||||||
|
return nil, &ParseError{fmt.Sprintf("Invalid key for [Interface] section"), key}
|
||||||
|
}
|
||||||
|
} else if parserState == inPeerSection {
|
||||||
|
switch key {
|
||||||
|
case "publickey":
|
||||||
|
k, err := parseKeyBase64(val)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
peer.PublicKey = *k
|
||||||
|
case "presharedkey":
|
||||||
|
k, err := parseKeyBase64(val)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
peer.PresharedKey = *k
|
||||||
|
case "allowedips":
|
||||||
|
addresses, err := splitList(val)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, address := range addresses {
|
||||||
|
a, err := parseIPCidr(address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
peer.AllowedIPs = append(peer.AllowedIPs, *a)
|
||||||
|
}
|
||||||
|
case "persistentkeepalive":
|
||||||
|
p, err := parsePersistentKeepalive(val)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
peer.PersistentKeepalive = p
|
||||||
|
case "endpoint":
|
||||||
|
e, err := parseEndpoint(val)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
peer.Endpoint = *e
|
||||||
|
case "mylogin":
|
||||||
|
conf.Interface.MyLogin = val
|
||||||
|
case "keysign":
|
||||||
|
conf.Interface.KeySign = val
|
||||||
|
default:
|
||||||
|
return nil, &ParseError{fmt.Sprintf("Invalid key for [Peer] section"), key}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
conf.maybeAddPeer(peer)
|
||||||
|
|
||||||
|
if !sawPrivateKey {
|
||||||
|
return nil, &ParseError{fmt.Sprintf("An interface must have a private key"), fmt.Sprintf("[none specified]")}
|
||||||
|
}
|
||||||
|
for _, p := range conf.Peers {
|
||||||
|
if p.PublicKey.IsZero() {
|
||||||
|
return nil, &ParseError{fmt.Sprintf("All peers must have public keys"), fmt.Sprintf("[none specified]")}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &conf, nil
|
||||||
|
}
|
Reference in New Issue
Block a user