// 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 }