/* SPDX-License-Identifier: MIT * * Copyright (C) 2019-2026 WireGuard LLC. All Rights Reserved. */ package conf import ( "encoding/base64" "net/netip" "strconv" "strings" "golang.org/x/text/encoding/unicode" "golang.zx2c4.com/wireguard/windows/l18n" "golang.zx2c4.com/wireguard/windows/phobos" ) type ParseError struct { why string offender string } func (e *ParseError) Error() string { return l18n.Sprintf("%s: %q", e.why, e.offender) } func parseIPCidr(s string) (netip.Prefix, error) { ipcidr, err := netip.ParsePrefix(s) if err == nil { return ipcidr, nil } addr, err := netip.ParseAddr(s) if err != nil { return netip.Prefix{}, &ParseError{l18n.Sprintf("Invalid IP address: "), s} } return netip.PrefixFrom(addr, addr.BitLen()), nil } func parseEndpoint(s string) (*Endpoint, error) { i := strings.LastIndexByte(s, ':') if i < 0 { return nil, &ParseError{l18n.Sprintf("Missing port from endpoint"), s} } host, portStr := s[:i], s[i+1:] if len(host) < 1 { return nil, &ParseError{l18n.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{l18n.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, err2 := netip.ParseAddr(host[1:end]) if err2 != nil || !maybeV6.Is6() { return nil, err } } else { return nil, err } host = host[1 : len(host)-1] } return &Endpoint{host, 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{l18n.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{l18n.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{l18n.Sprintf("Invalid persistent keepalive"), s} } return uint16(m), nil } func parseTableOff(s string) (bool, error) { if s == "off" { return true, nil } else if s == "auto" || s == "main" { return false, nil } _, err := strconv.ParseUint(s, 10, 32) return false, err } func parseKeyBase64(s string) (*Key, error) { k, err := base64.StdEncoding.DecodeString(s) if err != nil { return nil, &ParseError{l18n.Sprintf("Invalid key: %v", err), s} } if len(k) != KeyLength { return nil, &ParseError{l18n.Sprintf("Keys must decode to exactly 32 bytes"), s} } var key Key copy(key[:], k) return &key, nil } func splitList(s string) ([]string, error) { var out []string for split := range strings.SplitSeq(s, ",") { trim := strings.TrimSpace(split) if len(trim) == 0 { return nil, &ParseError{l18n.Sprintf("Two commas in a row"), s} } out = append(out, trim) } return out, nil } func parseUint16(s, what string) (uint16, error) { v, err := strconv.ParseUint(s, 10, 16) if err != nil { return 0, &ParseError{l18n.Sprintf("Invalid %s", what), s} } return uint16(v), nil } func parseObfuscationMode(s string) (ObfuscationMode, error) { switch strings.ToLower(s) { case "wireguard": return ObfuscationModeWireGuard, nil case "socks5": return ObfuscationModeSocks5, nil } return 0, &ParseError{l18n.Sprintf("Invalid obfuscator mode"), s} } func parseMasking(s string) (phobos.Masking, error) { masking, ok := phobos.ParseMasking(s) if !ok && !strings.EqualFold(strings.TrimSpace(s), "auto") { return 0, &ParseError{l18n.Sprintf("Invalid masking type"), s} } return masking, nil } func parseMediaSSRC(s string) (uint32, error) { base, digits := 10, s if rest, ok := strings.CutPrefix(strings.ToLower(s), "0x"); ok { base, digits = 16, rest } v, err := strconv.ParseUint(digits, base, 32) if err != nil { return 0, &ParseError{l18n.Sprintf("Invalid media SSRC"), s} } return uint32(v), nil } type parserState int const ( inInterfaceSection parserState = iota inPeerSection inObfuscationSection inSocks5Section notInASection ) func (c *Config) maybeAddPeer(p *Peer) { if p != nil { c.Peers = append(c.Peers, *p) } } func (o *Obfuscation) validate() error { if o.Target.IsEmpty() { return &ParseError{l18n.Sprintf("An obfuscator instance must have a target"), l18n.Sprintf("[none specified]")} } if len(o.Key) == 0 { return &ParseError{l18n.Sprintf("An obfuscator instance must have a key"), l18n.Sprintf("[none specified]")} } return nil } func (conf *Config) attachObfuscations(obfuscations []*Obfuscation) error { if len(obfuscations) == 0 { return nil } for _, o := range obfuscations { if err := o.validate(); err != nil { return err } } if obfuscations[0].Mode == ObfuscationModeSocks5 { if len(obfuscations) != 1 { return &ParseError{l18n.Sprintf("A SOCKS5 tunnel accepts exactly one [Instance] section"), l18n.Sprintf("%d given", len(obfuscations))} } if len(conf.Peers) != 0 { return &ParseError{l18n.Sprintf("A SOCKS5 tunnel must not declare peers"), l18n.Sprintf("%d given", len(conf.Peers))} } conf.Obfuscation = obfuscations[0] conf.applySocks5InterfaceDefaults() return nil } if len(conf.Peers) == 0 { return &ParseError{l18n.Sprintf("An [Instance] section requires a peer to attach to"), l18n.Sprintf("[none specified]")} } for _, o := range obfuscations { peer := conf.matchObfuscationPeer(o, len(obfuscations)) if peer == nil { return &ParseError{l18n.Sprintf("No peer matches the obfuscator source port"), strconv.Itoa(int(o.SourceListenPort))} } if peer.Obfuscation != nil { return &ParseError{l18n.Sprintf("A peer accepts only one [Instance] section"), peer.PublicKey.String()} } peer.Obfuscation = o } return nil } func (conf *Config) matchObfuscationPeer(o *Obfuscation, total int) *Peer { if total == 1 && len(conf.Peers) == 1 { return &conf.Peers[0] } for i := range conf.Peers { if conf.Peers[i].Endpoint.Port == o.SourceListenPort && o.SourceListenPort != 0 { return &conf.Peers[i] } } return nil } func FromWgQuick(s, name string) (*Config, error) { if !TunnelNameIsValid(name) { return nil, &ParseError{l18n.Sprintf("Tunnel name is not valid"), name} } lines := strings.Split(s, "\n") parserState := notInASection conf := Config{Name: name} sawPrivateKey := false var peer *Peer var obfuscation *Obfuscation var obfuscations []*Obfuscation var pendingComments []string for _, line := range lines { code, after, hasComment := strings.Cut(line, "#") comment := "" if hasComment { comment = "#" + strings.TrimRight(after, " \t\r") } stripped := strings.TrimSpace(code) if len(stripped) == 0 { if len(comment) != 0 { pendingComments = append(pendingComments, comment) } else { pendingComments = append(pendingComments, "") } continue } key, val, hasValue := strings.Cut(stripped, "=") key = strings.ToLower(strings.TrimSpace(key)) if key == "[interface]" && !hasValue { conf.maybeAddPeer(peer) parserState = inInterfaceSection h := &conf.Interface.Comments.Header h.Before = append(h.Before, pendingComments...) if len(comment) != 0 { if len(h.Suffix) == 0 { h.Suffix = comment } else { h.Suffix += " " + comment } } pendingComments = nil continue } if key == "[peer]" && !hasValue { conf.maybeAddPeer(peer) peer = &Peer{} peer.Comments.Header = Comments{Before: pendingComments, Suffix: comment} pendingComments = nil parserState = inPeerSection continue } if key == "[instance]" && !hasValue { conf.maybeAddPeer(peer) peer = nil obfuscation = &Obfuscation{Masking: phobos.MaskingNone, MaxDummy: phobos.DefaultMaxDummy} obfuscation.Comments.Header = Comments{Before: pendingComments, Suffix: comment} obfuscations = append(obfuscations, obfuscation) pendingComments = nil parserState = inObfuscationSection continue } if key == "[socks5]" && !hasValue { if obfuscation == nil { return nil, &ParseError{l18n.Sprintf("A [Socks5] section must follow an [Instance] section"), stripped} } obfuscation.Socks5Comments.Header = Comments{Before: pendingComments, Suffix: comment} pendingComments = nil parserState = inSocks5Section continue } if parserState == notInASection { return nil, &ParseError{l18n.Sprintf("Line must occur in a section"), stripped} } if !hasValue { return nil, &ParseError{l18n.Sprintf("Config key is missing an equals separator"), stripped} } switch key { case "preup", "postup", "predown", "postdown": _, val, _ = strings.Cut(line, "=") comment = "" } val = strings.TrimSpace(val) if len(val) == 0 { return nil, &ParseError{l18n.Sprintf("Key must have a value"), stripped} } section := &conf.Interface.Comments switch parserState { case inPeerSection: section = &peer.Comments case inObfuscationSection: section = &obfuscation.Comments case inSocks5Section: section = &obfuscation.Socks5Comments } if len(pendingComments) != 0 || len(comment) != 0 { if section.Lines == nil { section.Lines = make(map[string]Comments) } c := section.Lines[key] c.Before = append(c.Before, pendingComments...) if len(comment) != 0 { if len(c.Suffix) == 0 { c.Suffix = comment } else { c.Suffix += " " + comment } } section.Lines[key] = c } pendingComments = nil 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, err := netip.ParseAddr(address) if err != 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 case "table": tableOff, err := parseTableOff(val) if err != nil { return nil, err } conf.Interface.TableOff = tableOff default: return nil, &ParseError{l18n.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 default: return nil, &ParseError{l18n.Sprintf("Invalid key for [Peer] section"), key} } } else if parserState == inObfuscationSection { switch key { case "mode": m, err := parseObfuscationMode(val) if err != nil { return nil, err } obfuscation.Mode = m case "role": if !strings.EqualFold(val, "client") { return nil, &ParseError{l18n.Sprintf("Only the client obfuscator role is supported"), val} } case "source-lport": p, err := parsePort(val) if err != nil { return nil, err } obfuscation.SourceListenPort = p case "target": e, err := parseEndpoint(val) if err != nil { return nil, err } obfuscation.Target = *e case "key": obfuscation.Key = val case "masking": m, err := parseMasking(val) if err != nil { return nil, err } obfuscation.Masking = m if m == phobos.MaskingMEDIA && obfuscation.ObfuscateBytes == 0 { obfuscation.ObfuscateBytes = phobos.MediaObfuscateBytesDefault } case "obfuscate-bytes": b, err := parseUint16(val, "obfuscate-bytes") if err != nil { return nil, err } obfuscation.ObfuscateBytes = b case "max-dummy": d, err := parseUint16(val, "max-dummy") if err != nil { return nil, err } obfuscation.MaxDummy = d case "media-pt": pt, err := strconv.ParseUint(val, 10, 8) if err != nil || pt > 127 { return nil, &ParseError{l18n.Sprintf("Invalid RTP payload type"), val} } obfuscation.MediaPayloadType = uint8(pt) case "media-ssrc": ssrc, err := parseMediaSSRC(val) if err != nil { return nil, err } obfuscation.MediaSSRC = ssrc case "media-clock": clock, err := parseUint16(val, "media-clock") if err != nil || clock > 1000 { return nil, &ParseError{l18n.Sprintf("Invalid media clock"), val} } obfuscation.MediaClock = clock case "source-if", "verbose", "idle-timeout", "max-clients", "threads", "fwmark", "static-bindings", "socks5-users", "socks5-stats": default: return nil, &ParseError{l18n.Sprintf("Invalid key for [Instance] section"), key} } } else if parserState == inSocks5Section { switch key { case "login": obfuscation.Login = val case "password": obfuscation.Password = val default: return nil, &ParseError{l18n.Sprintf("Invalid key for [Socks5] section"), key} } } } conf.maybeAddPeer(peer) if err := conf.attachObfuscations(obfuscations); err != nil { return nil, err } pruneComments := func(run []string, dropLeading, dropTrailing bool) []string { var out []string for _, line := range run { if line == "" && len(out) != 0 && out[len(out)-1] == "" { continue } out = append(out, line) } if dropLeading && len(out) != 0 && out[0] == "" { out = out[1:] } if dropTrailing && len(out) != 0 && out[len(out)-1] == "" { out = out[:len(out)-1] } if len(out) == 0 || len(out) == 1 && out[0] == "" { return nil } return out } pruneSectionComments := func(s *SectionComments) { s.Header.Before = pruneComments(s.Header.Before, true, false) for key, c := range s.Lines { c.Before = pruneComments(c.Before, false, false) if len(c.Before) == 0 && len(c.Suffix) == 0 { delete(s.Lines, key) } else { s.Lines[key] = c } } if len(s.Lines) == 0 { s.Lines = nil } } conf.TrailingComments = pruneComments(pendingComments, false, true) pruneSectionComments(&conf.Interface.Comments) for i := range conf.Peers { pruneSectionComments(&conf.Peers[i].Comments) if o := conf.Peers[i].Obfuscation; o != nil { pruneSectionComments(&o.Comments) } } if conf.Obfuscation != nil { pruneSectionComments(&conf.Obfuscation.Comments) pruneSectionComments(&conf.Obfuscation.Socks5Comments) } if !sawPrivateKey && !conf.IsSocks5() { return nil, &ParseError{l18n.Sprintf("An interface must have a private key"), l18n.Sprintf("[none specified]")} } for _, p := range conf.Peers { if p.PublicKey.IsZero() { return nil, &ParseError{l18n.Sprintf("All peers must have public keys"), l18n.Sprintf("[none specified]")} } } return &conf, nil } func FromWgQuickWithUnknownEncoding(s, name string) (*Config, error) { c, firstErr := FromWgQuick(s, name) if firstErr == nil { return c, nil } for _, encoding := range unicode.All { decoded, err := encoding.NewDecoder().String(s) if err == nil { c, err := FromWgQuick(decoded, name) if err == nil { return c, nil } } } return nil, firstErr }