diff --git a/lib/wgconfig.go b/lib/wgconfig.go index 8f2b0f3..bef2b9f 100644 --- a/lib/wgconfig.go +++ b/lib/wgconfig.go @@ -1,14 +1,193 @@ +// TODO: Should split between encoder and decoder package lib import ( + "bufio" + "fmt" "io" + "net" + "strconv" + "strings" + "time" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" ) +const ( + CommentChar = "#" + AssignmentChar = "=" +) + +const ( + SectionNone = iota + SectionDevice + SectionPeer +) + +var sectionNames = []string{ + "None", + "Device", + "Peer", +} + +var ( + ErrUnknownSection = fmt.Errorf("unknown section") + ErrUnknownKey = fmt.Errorf("unknown key") + + ErrValueParse = fmt.Errorf("value parse failed") +) + // ReadConfig is yet another INI-like configuration file parser, but for WireGuard func ReadConfig(r io.Reader) (wgtypes.Config, error) { - tmp := make([]byte, 1000) - r.Read(tmp) - return wgtypes.Config{}, nil + scanner := bufio.NewScanner(r) + + config := wgtypes.Config{ReplacePeers: true} + section := SectionNone + + for scanner.Scan() { + text := scanner.Text() + line, _ := readConfigLine(text) + + s, k, v := parseLine(line) + + switch { + case insensetiveMatch(s, "Interface"): + section = SectionDevice + case insensetiveMatch(s, "Peer"): + section = SectionPeer + config.Peers = append(config.Peers, wgtypes.PeerConfig{ + ReplaceAllowedIPs: true, + }) + case len(s) > 0: + return config, fmt.Errorf("%w: %v", ErrUnknownSection, s) + } + + if len(k) == 0 { + continue + } + + // TODO: break out parsers into functions + switch section { + case SectionDevice: + switch { + case insensetiveMatch(k, "ListenPort"): + listenPort, err := strconv.ParseInt(v, 0, 0) + if err != nil { + return config, fmt.Errorf("%w: %w: %v=%v", ErrValueParse, err, k, v) + } + listenPortInt := int(listenPort) + config.ListenPort = &listenPortInt + case insensetiveMatch(k, "FwMark"): + fwMarkInt := 0 + if !insensetiveMatch(v, "off") { + fwMark, err := strconv.ParseInt(v, 0, 0) + if err != nil { + return config, fmt.Errorf("%w: %w: %v=%v", ErrValueParse, err, k, v) + } + fwMarkInt = int(fwMark) + } + config.FirewallMark = &fwMarkInt + case insensetiveMatch(k, "PrivateKey"): + key, err := wgtypes.ParseKey(v) + if err != nil { + return config, fmt.Errorf("%w: %w: %v=%v", ErrValueParse, err, k, v) + } + config.PrivateKey = &key + default: + return config, fmt.Errorf("%w: %v: %v", ErrUnknownKey, sectionNames[section], k) + } + case SectionPeer: + peer := &config.Peers[len(config.Peers)-1] + switch { + case insensetiveMatch(k, "Endpoint"): + endpoint, err := net.ResolveUDPAddr("udp", v) + if err != nil { + return config, fmt.Errorf("%w: %w: %v=%v", ErrValueParse, err, k, v) + } + peer.Endpoint = endpoint + case insensetiveMatch(k, "PublicKey"): + key, err := wgtypes.ParseKey(v) + if err != nil { + return config, fmt.Errorf("%w: %w: %v=%v", ErrValueParse, err, k, v) + } + peer.PublicKey = key + case insensetiveMatch(k, "AllowedIPs"): + allowedIPs, err := parseAllowedIPs(v) + if err != nil { + return config, fmt.Errorf("%w: %w: %v=%v", ErrValueParse, err, k, v) + } + peer.AllowedIPs = allowedIPs + case insensetiveMatch(k, "PersistentKeepalive"): + persistentKeepalive := int64(0) + var err error + if !insensetiveMatch(v, "off") { + persistentKeepalive, err = strconv.ParseInt(v, 0, 64) + if err != nil { + return config, fmt.Errorf("%w: %w: %v=%v", ErrValueParse, err, k, v) + } + } + if persistentKeepalive < 0 || persistentKeepalive > 65535 { + return config, fmt.Errorf("%w: Persistent keepalive interval is neither 0/off nor 1-65535: %v=%v", ErrValueParse, k, v) + } + persistentKeepaliveDuration := time.Duration(persistentKeepalive * int64(time.Second)) + peer.PersistentKeepaliveInterval = &persistentKeepaliveDuration + case insensetiveMatch(k, "PresharedKey"): + + default: + return config, fmt.Errorf("%w: %v: %v", ErrUnknownKey, sectionNames[section], k) + } + } + } + + return config, nil +} + +func readConfigLine(text string) (line, comments string) { + line = text + comments = "" + + comment := strings.Index(line, CommentChar) + if comment >= 0 { + line = text[:comment] + comments = text[comment+1:] + } + + line = strings.TrimSpace(line) + comments = strings.TrimSpace(comments) + return +} + +func parseLine(line string) (section, key, value string) { + if len(line) < 1 { + return "", "", "" + } + + if line[0] == '[' && line[len(line)-1] == ']' { + return line[1 : len(line)-1], "", "" + } + + assign := strings.Index(line, AssignmentChar) + if assign >= 0 { + return "", strings.TrimSpace(line[:assign]), strings.TrimSpace(line[assign+1:]) + } + + return "", "", "" +} + +func insensetiveMatch(a string, b string) bool { + return strings.ToLower(a) == strings.ToLower(b) +} + +func parseAllowedIPs(s string) ([]net.IPNet, error) { + parsedIPs := make([]net.IPNet, 0) + stringIPs := strings.Split(s, ",") + for _, stringIP := range stringIPs { + stringIP := strings.TrimSpace(stringIP) + _, parsedIP, err := net.ParseCIDR(stringIP) + if err != nil { + return parsedIPs, err + } + parsedIPs = append(parsedIPs, *parsedIP) + } + return parsedIPs, nil } diff --git a/lib/wgconfig_test.go b/lib/wgconfig_test.go index d5e4c93..a997d66 100644 --- a/lib/wgconfig_test.go +++ b/lib/wgconfig_test.go @@ -1,8 +1,10 @@ package lib import ( + "net" "strings" "testing" + "time" "github.com/google/go-cmp/cmp" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" @@ -18,21 +20,56 @@ PrivateKey = MITUgapB4QfRFF54ITXL3TaiYiSsVYkchqfjAXjxM10= [Peer] PublicKey = pjFx72IjbMh84SH1nq8Qfbl7HD5mSScHXCV1eISR7lk= AllowedIPs = 192.168.10.2/32, 2001:470:ed5d:a::2/128 +PersistentKeepalive = 80 [Peer] AllowedIPs = 192.168.10.40/32, 2001:470:ed5d:a::28/128 PublicKey = wXU+vSTdEoIwSi+Tmv35SCOFg17wCAwnmYxeQPpbzDg= ` -var testGoodConfig1Want = wgtypes.Config{} - func TestReadConfig1(t *testing.T) { buf := strings.NewReader(testGoodConfig1) got, err := ReadConfig(buf) if err != nil { t.Fatalf("config read failed: %w", err) } - if diff := cmp.Diff(testGoodConfig1Want, got); diff != "" { + + wantPrivateKey, _ := wgtypes.ParseKey("MITUgapB4QfRFF54ITXL3TaiYiSsVYkchqfjAXjxM10=") + wantListenPort := 3333 + wantPeer1PublicKey, _ := wgtypes.ParseKey("pjFx72IjbMh84SH1nq8Qfbl7HD5mSScHXCV1eISR7lk=") + _, wantPeer1AllowedIP1, _ := net.ParseCIDR("192.168.10.2/32") + _, wantPeer1AllowedIP2, _ := net.ParseCIDR("2001:470:ed5d:a::2/128") + wantPeer1PersistentKeepalive, _ := time.ParseDuration("80s") + wantPeer2PublicKey, _ := wgtypes.ParseKey("wXU+vSTdEoIwSi+Tmv35SCOFg17wCAwnmYxeQPpbzDg=") + _, wantPeer2AllowedIP1, _ := net.ParseCIDR("192.168.10.40/32") + _, wantPeer2AllowedIP2, _ := net.ParseCIDR("2001:470:ed5d:a::28/128") + + want := wgtypes.Config{ + PrivateKey: &wantPrivateKey, + ListenPort: &wantListenPort, + ReplacePeers: true, + Peers: []wgtypes.PeerConfig{ + wgtypes.PeerConfig{ + PublicKey: wantPeer1PublicKey, + ReplaceAllowedIPs: true, + AllowedIPs: []net.IPNet{ + *wantPeer1AllowedIP1, + *wantPeer1AllowedIP2, + }, + PersistentKeepaliveInterval: &wantPeer1PersistentKeepalive, + }, + wgtypes.PeerConfig{ + PublicKey: wantPeer2PublicKey, + ReplaceAllowedIPs: true, + AllowedIPs: []net.IPNet{ + *wantPeer2AllowedIP1, + *wantPeer2AllowedIP2, + }, + }, + }, + } + + if diff := cmp.Diff(want, got); diff != "" { t.Fatalf("returned config is not what is wanted: \n%s", diff) } }