Ground-Zerro / Phobos Public
Code Issues Pull requests Actions Releases View on GitHub ↗
11.6 KB go
/* SPDX-License-Identifier: MIT
 *
 * Copyright (C) 2019-2026 WireGuard LLC. All Rights Reserved.
 */

package conf

import (
	"net/netip"
	"reflect"
	"runtime"
	"strings"
	"testing"
)

const testInput = `
[Interface] 
Address = 10.192.122.1/24 
Address = 10.10.0.1/16 
PrivateKey = yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk= 
ListenPort = 51820  #comments don't matter

[Peer] 
PublicKey   =   xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=    
Endpoint = 192.95.5.67:1234 
AllowedIPs = 10.192.122.3/32, 10.192.124.1/24

[Peer] 
PublicKey = TrMvSoP4jYQlY6RIzBgbssQqY3vxI2Pi+y71lOWWXX0= 
Endpoint = [2607:5300:60:6b0::c05f:543]:2468 
AllowedIPs = 10.192.122.4/32, 192.168.0.0/16
PersistentKeepalive = 100

[Peer] 
PublicKey = gN65BkIKy1eCE9pP1wdc8ROUtkHLF2PfAqYdyYBz6EA= 
PresharedKey = TrMvSoP4jYQlY6RIzBgbssQqY3vxI2Pi+y71lOWWXX0= 
Endpoint = test.wireguard.com:18981 
AllowedIPs = 10.10.10.230/32`

func noError(t *testing.T, err error) bool {
	if err == nil {
		return true
	}
	_, fn, line, _ := runtime.Caller(1)
	t.Errorf("Error at %s:%d: %#v", fn, line, err)
	return false
}

func equal(t *testing.T, expected, actual any) bool {
	if reflect.DeepEqual(expected, actual) {
		return true
	}
	_, fn, line, _ := runtime.Caller(1)
	t.Errorf("Failed equals at %s:%d\nactual   %#v\nexpected %#v", fn, line, actual, expected)
	return false
}

func lenTest(t *testing.T, actualO any, expected int) bool {
	actual := reflect.ValueOf(actualO).Len()
	if reflect.DeepEqual(expected, actual) {
		return true
	}
	_, fn, line, _ := runtime.Caller(1)
	t.Errorf("Wrong length at %s:%d\nactual   %#v\nexpected %#v", fn, line, actual, expected)
	return false
}

func contains(t *testing.T, list, element any) bool {
	listValue := reflect.ValueOf(list)
	for i := 0; i < listValue.Len(); i++ {
		if reflect.DeepEqual(listValue.Index(i).Interface(), element) {
			return true
		}
	}
	_, fn, line, _ := runtime.Caller(1)
	t.Errorf("Error %s:%d\nelement not found: %#v", fn, line, element)
	return false
}

func TestFromWgQuick(t *testing.T) {
	conf, err := FromWgQuick(testInput, "test")
	if noError(t, err) {
		lenTest(t, conf.Interface.Addresses, 2)
		contains(t, conf.Interface.Addresses, netip.PrefixFrom(netip.AddrFrom4([4]byte{10, 10, 0, 1}), 16))
		contains(t, conf.Interface.Addresses, netip.PrefixFrom(netip.AddrFrom4([4]byte{10, 192, 122, 1}), 24))
		equal(t, "yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=", conf.Interface.PrivateKey.String())
		equal(t, uint16(51820), conf.Interface.ListenPort)

		lenTest(t, conf.Peers, 3)
		lenTest(t, conf.Peers[0].AllowedIPs, 2)
		equal(t, Endpoint{Host: "192.95.5.67", Port: 1234}, conf.Peers[0].Endpoint)
		equal(t, "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=", conf.Peers[0].PublicKey.String())

		lenTest(t, conf.Peers[1].AllowedIPs, 2)
		equal(t, Endpoint{Host: "2607:5300:60:6b0::c05f:543", Port: 2468}, conf.Peers[1].Endpoint)
		equal(t, "TrMvSoP4jYQlY6RIzBgbssQqY3vxI2Pi+y71lOWWXX0=", conf.Peers[1].PublicKey.String())
		equal(t, uint16(100), conf.Peers[1].PersistentKeepalive)

		lenTest(t, conf.Peers[2].AllowedIPs, 1)
		equal(t, Endpoint{Host: "test.wireguard.com", Port: 18981}, conf.Peers[2].Endpoint)
		equal(t, "gN65BkIKy1eCE9pP1wdc8ROUtkHLF2PfAqYdyYBz6EA=", conf.Peers[2].PublicKey.String())
		equal(t, "TrMvSoP4jYQlY6RIzBgbssQqY3vxI2Pi+y71lOWWXX0=", conf.Peers[2].PresharedKey.String())
	}
}

func TestComments(t *testing.T) {
	const input = `# top of file
# second line

[Interface] # the interface
PrivateKey = yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=
# which port
ListenPort = 51820 # inline port
Address = 10.0.0.1/24

# the only peer
[Peer]
PublicKey = xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=
AllowedIPs = 0.0.0.0/0 # everything
# trailing note
`
	c, err := FromWgQuick(input, "test")
	if !noError(t, err) {
		return
	}

	equal(t, []string{"# top of file", "# second line", ""}, c.Interface.Comments.Header.Before)
	equal(t, "# the interface", c.Interface.Comments.Header.Suffix)
	equal(t, []string{"# which port"}, c.Interface.Comments.Lines["listenport"].Before)
	equal(t, "# inline port", c.Interface.Comments.Lines["listenport"].Suffix)
	equal(t, []string{"# the only peer"}, c.Peers[0].Comments.Header.Before)
	equal(t, "# everything", c.Peers[0].Comments.Lines["allowedips"].Suffix)
	equal(t, []string{"# trailing note"}, c.TrailingComments)

	c2, err := FromWgQuick("[Interface]\nPrivateKey = yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=\nPostUp = echo '#1 done'\n", "test")
	if noError(t, err) {
		equal(t, "echo '#1 done'", c2.Interface.PostUp)
		equal(t, "", c2.Interface.Comments.Lines["postup"].Suffix)
	}

	serialized := c.ToWgQuick()
	for _, want := range []string{
		"# top of file\n# second line\n\n[Interface] # the interface\n",
		"# which port\nListenPort = 51820 # inline port\n",
		"# the only peer\n[Peer]\n",
		"AllowedIPs = 0.0.0.0/0 # everything\n",
		"# trailing note\n",
	} {
		if !strings.Contains(serialized, want) {
			t.Errorf("serialized config missing %q in:\n%s", want, serialized)
		}
	}

	reparsed, err := FromWgQuick(serialized, "test")
	if noError(t, err) {
		equal(t, serialized, reparsed.ToWgQuick())
		equal(t, c, reparsed)
	}
}

func TestCommentBlankLines(t *testing.T) {
	const input = `# Header line one

# Header line two
[Interface]
PrivateKey = yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=

# blank line precedes this comment
ListenPort = 51820
Address = 10.0.0.1/24


# multiple blank lines above collapse to one
[Peer]
PublicKey = xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=
AllowedIPs = 0.0.0.0/0
`
	c, err := FromWgQuick(input, "test")
	if !noError(t, err) {
		return
	}

	equal(t, []string{"# Header line one", "", "# Header line two"}, c.Interface.Comments.Header.Before)
	equal(t, []string{"", "# blank line precedes this comment"}, c.Interface.Comments.Lines["listenport"].Before)
	equal(t, []string{"# multiple blank lines above collapse to one"}, c.Peers[0].Comments.Header.Before)

	serialized := c.ToWgQuick()
	for _, want := range []string{
		"# Header line one\n\n# Header line two\n[Interface]\n",
		"\n# blank line precedes this comment\nListenPort = 51820\n",
		"\n# multiple blank lines above collapse to one\n[Peer]\n",
	} {
		if !strings.Contains(serialized, want) {
			t.Errorf("serialized config missing %q in:\n%s", want, serialized)
		}
	}
	if strings.Contains(serialized, "\n\n\n") {
		t.Errorf("serialized config has a run of blank lines:\n%s", serialized)
	}

	reparsed, err := FromWgQuick(serialized, "test")
	if noError(t, err) {
		equal(t, serialized, reparsed.ToWgQuick())
		equal(t, c, reparsed)
	}
}

func TestCommentRepeatedKey(t *testing.T) {
	const input = `[Interface]
PrivateKey = yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=
# a

Address = 10.0.0.1/24 # home

# b
Address = 10.0.0.2/24 # work
`
	c, err := FromWgQuick(input, "test")
	if !noError(t, err) {
		return
	}
	lenTest(t, c.Interface.Addresses, 2)
	addr := c.Interface.Comments.Lines["address"]
	equal(t, []string{"# a", "", "# b"}, addr.Before)
	equal(t, "# home # work", addr.Suffix)
	serialized := c.ToWgQuick()
	if !strings.Contains(serialized, "# a\n\n# b\nAddress = 10.0.0.1/24, 10.0.0.2/24 # home # work\n") {
		t.Errorf("merged repeated-key comments wrong in:\n%s", serialized)
	}

	c2, err := FromWgQuick("# one\n[Interface] # h1\nPrivateKey = yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=\n# two\n[Interface] # h2\nListenPort = 51820\n", "test")
	if noError(t, err) {
		equal(t, []string{"# one", "# two"}, c2.Interface.Comments.Header.Before)
		equal(t, "# h1 # h2", c2.Interface.Comments.Header.Suffix)
	}
}

func TestCommentDefaultValuedKeys(t *testing.T) {
	for _, input := range []string{
		"[Interface]\nPrivateKey = yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=\n# note\nListenPort = 0 # inline\n",
		"[Interface]\nPrivateKey = yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=\n# note\nTable = auto # inline\n",
		"[Interface]\nPrivateKey = yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=\n[Peer]\nPublicKey = xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=\nAllowedIPs = 0.0.0.0/0\n# note\nPersistentKeepalive = off # inline\n",
		"[Interface]\nPrivateKey = yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=\n[Peer]\nPublicKey = xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=\nAllowedIPs = 0.0.0.0/0\n# note\nPresharedKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n",
	} {
		c, err := FromWgQuick(input, "test")
		if !noError(t, err) {
			continue
		}
		serialized := c.ToWgQuick()
		if !strings.Contains(serialized, "# note") {
			t.Errorf("comment on a default-valued key was dropped:\n--input--\n%s--output--\n%s", input, serialized)
		}
		reparsed, err := FromWgQuick(serialized, "test")
		if noError(t, err) {
			equal(t, c, reparsed)
		}
	}
}

func TestRedactClearsComments(t *testing.T) {
	input := "[Interface]\nPrivateKey = yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk= # backup SECRET\n" +
		"# note SECRET\n[Peer]\nPublicKey = xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=\nAllowedIPs = 0.0.0.0/0\n# trailing SECRET\n"
	c, err := FromWgQuick(input, "test")
	if !noError(t, err) {
		return
	}
	c.Redact()
	if out := c.ToWgQuick(); strings.Contains(out, "SECRET") {
		t.Errorf("Redact left comment text in output:\n%s", out)
	}
}

func FuzzRoundTrip(f *testing.F) {
	f.Add(testInput)
	f.Add("# top of file\n# second line\n\n[Interface] # the interface\n" +
		"PrivateKey = yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=\n# which port\n" +
		"ListenPort = 51820 # inline port\nAddress = 10.0.0.1/24\n\n# the only peer\n[Peer]\n" +
		"PublicKey = xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=\nAllowedIPs = 0.0.0.0/0 # everything\n# trailing note\n")
	f.Add("[Interface]\nPrivateKey = yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=\n# a\n\n" +
		"Address = 10.0.0.1/24 # home\n\n# b\nAddress = 10.0.0.2/24 # work\n")
	f.Add("[Interface]\nPrivateKey = yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=\n# p\nListenPort = 0 # x\n" +
		"# t\nTable = auto # y\n[Peer]\nPublicKey = xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=\n" +
		"AllowedIPs = 0.0.0.0/0\n# k\nPersistentKeepalive = off # z\n")
	f.Add("[Interface]\nPrivateKey = yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=\nDNS = 1.1.1.1, home.arpa\nMTU = 1280\n" +
		"# pre\nPreUp = echo a=b #1\nPostUp = ip route add # x\nPreDown = echo down\nPostDown = echo # done\n")

	f.Fuzz(func(t *testing.T, s string) {
		c, err := FromWgQuick(s, "test")
		if err != nil {
			return
		}
		serialized := c.ToWgQuick()
		reparsed, err := FromWgQuick(serialized, "test")
		if err != nil {
			t.Fatalf("reserialized config no longer parses: %v\n%s", err, serialized)
		}
		if got := reparsed.ToWgQuick(); got != serialized {
			t.Errorf("serialization is not idempotent\n--first--\n%s--second--\n%s", serialized, got)
		}
		if !reflect.DeepEqual(c, reparsed) {
			t.Errorf("round-trip changed the parsed config\n--input--\n%s--serialized--\n%s", s, serialized)
		}
		if strings.Contains(serialized, "\n\n\n") {
			t.Errorf("output has consecutive blank lines:\n%s", serialized)
		}
		if strings.HasPrefix(serialized, "\n") {
			t.Errorf("output begins with a blank line:\n%s", serialized)
		}
	})
}

func TestParseEndpoint(t *testing.T) {
	_, err := parseEndpoint("[192.168.42.0:]:51880")
	if err == nil {
		t.Error("Error was expected")
	}
	e, err := parseEndpoint("192.168.42.0:51880")
	if noError(t, err) {
		equal(t, "192.168.42.0", e.Host)
		equal(t, uint16(51880), e.Port)
	}
	e, err = parseEndpoint("test.wireguard.com:18981")
	if noError(t, err) {
		equal(t, "test.wireguard.com", e.Host)
		equal(t, uint16(18981), e.Port)
	}
	e, err = parseEndpoint("[2607:5300:60:6b0::c05f:543]:2468")
	if noError(t, err) {
		equal(t, "2607:5300:60:6b0::c05f:543", e.Host)
		equal(t, uint16(2468), e.Port)
	}
	_, err = parseEndpoint("[::::::invalid:18981")
	if err == nil {
		t.Error("Error was expected")
	}
	_, err = parseEndpoint("::0")
	if err == nil {
		t.Error("Error was expected")
	}
}