summaryrefslogtreecommitdiff
path: root/pkg/wgconf/wgconf.go
blob: 5eac20db493dec3527da7eb2f2eb2bc0011313d4 (plain)
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
138
139
140
141
// Package wgconf provides functionality for parsing WireGuard .conf files.
//
// Parsing Logic:
// wgconf implements a robust parser for WireGuard configuration files, supporting:
// - Case-insensitive section ([Interface], [Peer]) and key names.
// - Inline and block comments starting with # or ;.
// - Multi-value fields (e.g., AllowedIPs) separated by commas.
// - Flexible whitespace handling around keys and values.
package wgconf

import (
	"bufio"
	"fmt"
	"os"
	"strings"
)

// Config represents a parsed WireGuard configuration file.
type Config struct {
	// PrivateKey is the local interface's private key.
	PrivateKey string
	// Address is the local interface's IP address.
	Address string
	// DNS is the DNS server to be used when the tunnel is active.
	DNS string
	// Peers is the list of remote peers defined in the configuration.
	Peers []Peer
}

// Peer represents a WireGuard peer.
type Peer struct {
	// PublicKey is the remote peer's public key.
	PublicKey string
	// Endpoint is the public IP and port of the remote peer.
	Endpoint string
	// AllowedIPs is the list of IP addresses/networks allowed for this peer.
	AllowedIPs []string
}

// stripComment removes inline and block comments from a line, keeping spaces intact otherwise.
func stripComment(line string) string {
	line = strings.TrimSpace(line)
	if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") {
		return ""
	}

	// Find the first occurrence of '#' or ';' preceded by a whitespace character.
	// This protects characters like '#' if they are part of a key/value with no leading whitespace.
	for i := 1; i < len(line); i++ {
		if (line[i] == '#' || line[i] == ';') && (line[i-1] == ' ' || line[i-1] == '\t') {
			return strings.TrimSpace(line[:i])
		}
	}
	return line
}

// Parse reads a WireGuard .conf file and returns a Config struct.
func Parse(path string) (*Config, error) {
	file, err := os.Open(path)
	if err != nil {
		return nil, fmt.Errorf("failed to open config file: %w", err)
	}
	defer func() {
		if err := file.Close(); err != nil {
			// We use a simple print here because we are in a defer
			fmt.Printf("warning: failed to close config file %s: %v\n", path, err)
		}
	}()

	cfg := &Config{}
	var currentPeer *Peer

	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		line := stripComment(scanner.Text())
		if line == "" {
			continue
		}

		if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
			section := strings.ToLower(strings.Trim(line, "[] \t"))
			if section == "peer" {
				if currentPeer != nil {
					cfg.Peers = append(cfg.Peers, *currentPeer)
				}
				currentPeer = &Peer{}
			} else {
				if currentPeer != nil {
					cfg.Peers = append(cfg.Peers, *currentPeer)
					currentPeer = nil
				}
			}
			continue
		}

		parts := strings.SplitN(line, "=", 2)
		if len(parts) != 2 {
			return nil, fmt.Errorf("invalid line format: %s", line)
		}

		key := strings.ToLower(strings.TrimSpace(parts[0]))
		val := strings.TrimSpace(parts[1])

		if currentPeer != nil {
			switch key {
			case "publickey":
				currentPeer.PublicKey = val
			case "endpoint":
				currentPeer.Endpoint = val
			case "allowedips":
				var ips []string
				for _, ip := range strings.Split(val, ",") {
					trimmed := strings.TrimSpace(ip)
					if trimmed != "" {
						ips = append(ips, trimmed)
					}
				}
				currentPeer.AllowedIPs = ips
			}
		} else {
			switch key {
			case "privatekey":
				cfg.PrivateKey = val
			case "address":
				cfg.Address = val
			case "dns":
				cfg.DNS = val
			}
		}
	}

	if currentPeer != nil {
		cfg.Peers = append(cfg.Peers, *currentPeer)
	}

	if err := scanner.Err(); err != nil {
		return nil, fmt.Errorf("error reading config file: %w", err)
	}

	return cfg, nil
}