├── .gitignore ├── internal ├── wgtest │ ├── doc.go │ └── wgtest.go ├── wgfreebsd │ ├── internal │ │ ├── nv │ │ │ ├── types.go │ │ │ ├── doc.go │ │ │ ├── nvlist_test.go │ │ │ ├── decode.go │ │ │ └── encode.go │ │ └── wgh │ │ │ ├── doc.go │ │ │ ├── defs_freebsd_386.go │ │ │ ├── defs_freebsd_arm.go │ │ │ ├── defs_freebsd_amd64.go │ │ │ ├── defs_freebsd_arm64.go │ │ │ ├── generate.sh │ │ │ └── defs.go │ ├── doc.go │ └── client_freebsd.go ├── wgopenbsd │ ├── internal │ │ └── wgh │ │ │ ├── doc.go │ │ │ ├── generate.sh │ │ │ ├── defs_openbsd_386.go │ │ │ ├── defs_openbsd_amd64.go │ │ │ ├── defs_openbsd_arm64.go │ │ │ ├── defs_openbsd_arm.go │ │ │ └── defs.go │ ├── doc.go │ ├── client_openbsd_test.go │ └── client_openbsd.go ├── wginternal │ ├── doc.go │ └── client.go ├── wglinux │ ├── doc.go │ ├── client_linux.go │ ├── client_linux_test.go │ ├── configure_linux.go │ ├── parse_linux.go │ └── configure_linux_test.go ├── wguser │ ├── doc.go │ ├── conn_unix.go │ ├── conn_windows_test.go │ ├── conn_unix_test.go │ ├── conn_windows.go │ ├── client.go │ ├── configure.go │ ├── client_test.go │ ├── configure_test.go │ ├── parse_test.go │ └── parse.go └── wgwindows │ ├── internal │ └── ioctl │ │ ├── winipcfg_windows.go │ │ └── configuration_windows.go │ └── client_windows.go ├── wgtypes ├── doc.go ├── errors.go ├── types_test.go └── types.go ├── doc.go ├── os_userspace.go ├── go.mod ├── .github └── workflows │ ├── linux-test.yml │ ├── static-analysis.yml │ └── linux-integration-test.yml ├── os_windows.go ├── .builds ├── freebsd.yml └── openbsd.yml ├── os_freebsd.go ├── os_openbsd.go ├── CONTRIBUTING.md ├── os_linux.go ├── LICENSE.md ├── .cibuild.sh ├── README.md ├── go.sum ├── cmd └── wgctrl │ └── main.go ├── client.go ├── client_test.go └── client_integration_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | cmd/wgctrl/wgctrl 2 | *.test 3 | -------------------------------------------------------------------------------- /internal/wgtest/doc.go: -------------------------------------------------------------------------------- 1 | // Package wgtest contains shared testing utilities for package wgctrl. 2 | package wgtest 3 | -------------------------------------------------------------------------------- /internal/wgfreebsd/internal/nv/types.go: -------------------------------------------------------------------------------- 1 | //go:build freebsd 2 | // +build freebsd 3 | 4 | package nv 5 | 6 | type List map[string]interface{} 7 | -------------------------------------------------------------------------------- /wgtypes/doc.go: -------------------------------------------------------------------------------- 1 | // Package wgtypes provides shared types for the wgctrl family of packages. 2 | package wgtypes // import "golang.zx2c4.com/wireguard/wgctrl/wgtypes" 3 | -------------------------------------------------------------------------------- /internal/wgfreebsd/internal/wgh/doc.go: -------------------------------------------------------------------------------- 1 | // Package wgh is an auto-generated package which contains constants and 2 | // types used to access WireGuard information using ioctl calls. 3 | package wgh 4 | -------------------------------------------------------------------------------- /internal/wgopenbsd/internal/wgh/doc.go: -------------------------------------------------------------------------------- 1 | // Package wgh is an auto-generated package which contains constants and 2 | // types used to access WireGuard information using ioctl calls. 3 | package wgh 4 | -------------------------------------------------------------------------------- /internal/wgfreebsd/internal/nv/doc.go: -------------------------------------------------------------------------------- 1 | // Package nv marshals and unmarshals Go maps to/from FreeBSDs nv(9) name/value lists 2 | // See: https://www.freebsd.org/cgi/man.cgi?query=nv&sektion=9 3 | package nv 4 | -------------------------------------------------------------------------------- /internal/wginternal/doc.go: -------------------------------------------------------------------------------- 1 | // Package wginternal contains shared internal types for wgctrl. 2 | // 3 | // This package is internal-only and not meant for end users to consume. 4 | // Please use package wgctrl (an abstraction over this package) instead. 5 | package wginternal 6 | -------------------------------------------------------------------------------- /internal/wgfreebsd/doc.go: -------------------------------------------------------------------------------- 1 | // Package wgfreebsd provides internal access to FreeBSD's WireGuard 2 | // ioctl interface. 3 | // 4 | // This package is internal-only and not meant for end users to consume. 5 | // Please use package wgctrl (an abstraction over this package) instead. 6 | package wgfreebsd 7 | -------------------------------------------------------------------------------- /internal/wglinux/doc.go: -------------------------------------------------------------------------------- 1 | // Package wglinux provides internal access to Linux's WireGuard generic 2 | // netlink interface. 3 | // 4 | // This package is internal-only and not meant for end users to consume. 5 | // Please use package wgctrl (an abstraction over this package) instead. 6 | package wglinux 7 | -------------------------------------------------------------------------------- /internal/wgopenbsd/doc.go: -------------------------------------------------------------------------------- 1 | // Package wgopenbsd provides internal access to OpenBSD's WireGuard 2 | // ioctl interface. 3 | // 4 | // This package is internal-only and not meant for end users to consume. 5 | // Please use package wgctrl (an abstraction over this package) instead. 6 | package wgopenbsd 7 | -------------------------------------------------------------------------------- /internal/wguser/doc.go: -------------------------------------------------------------------------------- 1 | // Package wguser provides internal access to the userspace WireGuard 2 | // configuration protocol interface. 3 | // 4 | // This package is internal-only and not meant for end users to consume. 5 | // Please use package wgctrl (an abstraction over this package) instead. 6 | package wguser 7 | -------------------------------------------------------------------------------- /wgtypes/errors.go: -------------------------------------------------------------------------------- 1 | package wgtypes 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | // ErrUpdateOnlyNotSupported is returned due to missing kernel support of 8 | // the PeerConfig UpdateOnly flag. 9 | var ErrUpdateOnlyNotSupported = errors.New("the UpdateOnly flag is not supported by this platform") 10 | 11 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package wgctrl enables control of WireGuard devices on multiple platforms. 2 | // 3 | // For more information on WireGuard, please see https://www.wireguard.com/. 4 | // 5 | // This package implements WireGuard configuration protocol operations, enabling 6 | // the configuration of existing WireGuard devices. Operations such as creating 7 | // WireGuard devices, or applying IP addresses to those devices, are out of scope 8 | // for this package. 9 | package wgctrl // import "golang.zx2c4.com/wireguard/wgctrl" 10 | -------------------------------------------------------------------------------- /os_userspace.go: -------------------------------------------------------------------------------- 1 | //go:build !linux && !openbsd && !windows && !freebsd 2 | // +build !linux,!openbsd,!windows,!freebsd 3 | 4 | package wgctrl 5 | 6 | import ( 7 | "golang.zx2c4.com/wireguard/wgctrl/internal/wginternal" 8 | "golang.zx2c4.com/wireguard/wgctrl/internal/wguser" 9 | ) 10 | 11 | // newClients configures wginternal.Clients for systems which only support 12 | // userspace WireGuard implementations. 13 | func newClients() ([]wginternal.Client, error) { 14 | c, err := wguser.New() 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | return []wginternal.Client{c}, nil 20 | } 21 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module golang.zx2c4.com/wireguard/wgctrl 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/google/go-cmp v0.6.0 7 | github.com/mdlayher/genetlink v1.3.2 8 | github.com/mdlayher/netlink v1.7.2 9 | github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 10 | golang.org/x/crypto v0.31.0 11 | golang.org/x/sys v0.28.0 12 | golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 13 | ) 14 | 15 | require ( 16 | github.com/josharian/native v1.1.0 // indirect 17 | github.com/mdlayher/socket v0.5.1 // indirect 18 | golang.org/x/net v0.33.0 // indirect 19 | golang.org/x/sync v0.10.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /internal/wgfreebsd/internal/wgh/defs_freebsd_386.go: -------------------------------------------------------------------------------- 1 | //go:build freebsd && 386 2 | // +build freebsd,386 3 | 4 | // Code generated by cmd/cgo -godefs; DO NOT EDIT. 5 | // cgo -godefs defs.go 6 | 7 | package wgh 8 | 9 | const ( 10 | SizeofIfgreq = 0x10 11 | 12 | SIOCGWG = 0xc01869d3 13 | SIOCSWG = 0xc01869d2 14 | ) 15 | 16 | type Ifgroupreq struct { 17 | Name [16]byte 18 | Len uint32 19 | Pad1 [0]byte 20 | Groups *Ifgreq 21 | Pad2 [12]byte 22 | } 23 | 24 | type Ifgreq struct { 25 | Ifgrqu [16]byte 26 | } 27 | 28 | type WGDataIO struct { 29 | Name [16]byte 30 | Data *byte 31 | Size uint32 32 | } 33 | -------------------------------------------------------------------------------- /internal/wgfreebsd/internal/wgh/defs_freebsd_arm.go: -------------------------------------------------------------------------------- 1 | //go:build freebsd && arm 2 | // +build freebsd,arm 3 | 4 | // Code generated by cmd/cgo -godefs; DO NOT EDIT. 5 | // cgo -godefs defs.go 6 | 7 | package wgh 8 | 9 | const ( 10 | SizeofIfgreq = 0x10 11 | 12 | SIOCGWG = 0xc01c69d3 13 | SIOCSWG = 0xc01c69d2 14 | ) 15 | 16 | type Ifgroupreq struct { 17 | Name [16]byte 18 | Len uint32 19 | Pad1 [0]byte 20 | Groups *Ifgreq 21 | Pad2 [12]byte 22 | } 23 | 24 | type Ifgreq struct { 25 | Ifgrqu [16]byte 26 | } 27 | 28 | type WGDataIO struct { 29 | Name [16]byte 30 | Data *byte 31 | Size uint64 32 | } 33 | -------------------------------------------------------------------------------- /internal/wgfreebsd/internal/wgh/defs_freebsd_amd64.go: -------------------------------------------------------------------------------- 1 | //go:build freebsd && amd64 2 | // +build freebsd,amd64 3 | 4 | // Code generated by cmd/cgo -godefs; DO NOT EDIT. 5 | // cgo -godefs defs.go 6 | 7 | package wgh 8 | 9 | const ( 10 | SizeofIfgreq = 0x10 11 | 12 | SIOCGWG = 0xc02069d3 13 | SIOCSWG = 0xc02069d2 14 | ) 15 | 16 | type Ifgroupreq struct { 17 | Name [16]byte 18 | Len uint32 19 | Pad1 [4]byte 20 | Groups *Ifgreq 21 | Pad2 [8]byte 22 | } 23 | 24 | type Ifgreq struct { 25 | Ifgrqu [16]byte 26 | } 27 | 28 | type WGDataIO struct { 29 | Name [16]byte 30 | Data *byte 31 | Size uint64 32 | } 33 | -------------------------------------------------------------------------------- /internal/wgfreebsd/internal/wgh/defs_freebsd_arm64.go: -------------------------------------------------------------------------------- 1 | //go:build freebsd && arm64 2 | // +build freebsd,arm64 3 | 4 | // Code generated by cmd/cgo -godefs; DO NOT EDIT. 5 | // cgo -godefs defs.go 6 | 7 | package wgh 8 | 9 | const ( 10 | SizeofIfgreq = 0x10 11 | 12 | SIOCGWG = 0xc02069d3 13 | SIOCSWG = 0xc02069d2 14 | ) 15 | 16 | type Ifgroupreq struct { 17 | Name [16]byte 18 | Len uint32 19 | Pad1 [4]byte 20 | Groups *Ifgreq 21 | Pad2 [8]byte 22 | } 23 | 24 | type Ifgreq struct { 25 | Ifgrqu [16]byte 26 | } 27 | 28 | type WGDataIO struct { 29 | Name [16]byte 30 | Data *byte 31 | Size uint64 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/linux-test.yml: -------------------------------------------------------------------------------- 1 | name: Linux Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | pull_request: 8 | branches: 9 | - '*' 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | matrix: 15 | go-version: ["1.20"] 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Set up Go 20 | uses: actions/setup-go@v2 21 | with: 22 | go-version: ${{ matrix.go-version }} 23 | id: go 24 | 25 | - name: Check out code into the Go module directory 26 | uses: actions/checkout@v2 27 | 28 | - name: Run tests 29 | run: go test -race ./... 30 | -------------------------------------------------------------------------------- /internal/wginternal/client.go: -------------------------------------------------------------------------------- 1 | package wginternal 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | 7 | "golang.zx2c4.com/wireguard/wgctrl/wgtypes" 8 | ) 9 | 10 | // ErrReadOnly indicates that the driver backing a device is read-only. It is 11 | // a sentinel value used in integration tests. 12 | // TODO(mdlayher): consider exposing in API. 13 | var ErrReadOnly = errors.New("driver is read-only") 14 | 15 | // A Client is a type which can control a WireGuard device. 16 | type Client interface { 17 | io.Closer 18 | Devices() ([]*wgtypes.Device, error) 19 | Device(name string) (*wgtypes.Device, error) 20 | ConfigureDevice(name string, cfg wgtypes.Config) error 21 | } 22 | -------------------------------------------------------------------------------- /os_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package wgctrl 5 | 6 | import ( 7 | "golang.zx2c4.com/wireguard/wgctrl/internal/wginternal" 8 | "golang.zx2c4.com/wireguard/wgctrl/internal/wguser" 9 | "golang.zx2c4.com/wireguard/wgctrl/internal/wgwindows" 10 | ) 11 | 12 | // newClients configures wginternal.Clients for Windows systems. 13 | func newClients() ([]wginternal.Client, error) { 14 | var clients []wginternal.Client 15 | 16 | // Windows has an in-kernel WireGuard implementation. 17 | kc := wgwindows.New() 18 | clients = append(clients, kc) 19 | 20 | uc, err := wguser.New() 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | clients = append(clients, uc) 26 | return clients, nil 27 | } 28 | -------------------------------------------------------------------------------- /.builds/freebsd.yml: -------------------------------------------------------------------------------- 1 | image: freebsd/latest 2 | packages: 3 | - go 4 | - bash 5 | - sudo 6 | - wireguard 7 | sources: 8 | - https://github.com/WireGuard/wgctrl-go 9 | environment: 10 | GO111MODULE: "on" 11 | GOBIN: "/home/build/go/bin" 12 | CGO_ENABLED: "1" 13 | tasks: 14 | - setup-wireguard: | 15 | ./wgctrl-go/.cibuild.sh 16 | - build: | 17 | go version 18 | go install honnef.co/go/tools/cmd/staticcheck@latest 19 | cd wgctrl-go/ 20 | diff -u <(echo -n) <(/usr/local/go/bin/gofmt -d -s .) 21 | go vet ./... 22 | $GOBIN/staticcheck ./... 23 | go test -v -race ./... 24 | go test -c -race . 25 | # Use wireguard-go for additional testing. 26 | sudo /usr/local/bin/wireguard-go wguser0 27 | sudo WGCTRL_INTEGRATION=yesreallydoit ./wgctrl.test -test.v -test.run TestIntegration 28 | -------------------------------------------------------------------------------- /os_freebsd.go: -------------------------------------------------------------------------------- 1 | //go:build freebsd 2 | // +build freebsd 3 | 4 | package wgctrl 5 | 6 | import ( 7 | "golang.zx2c4.com/wireguard/wgctrl/internal/wgfreebsd" 8 | "golang.zx2c4.com/wireguard/wgctrl/internal/wginternal" 9 | "golang.zx2c4.com/wireguard/wgctrl/internal/wguser" 10 | ) 11 | 12 | // newClients configures wginternal.Clients for FreeBSD systems. 13 | func newClients() ([]wginternal.Client, error) { 14 | var clients []wginternal.Client 15 | 16 | // FreeBSD has an in-kernel WireGuard implementation. Determine if it is 17 | // available and make use of it if so. 18 | kc, ok, err := wgfreebsd.New() 19 | if err != nil { 20 | return nil, err 21 | } 22 | if ok { 23 | clients = append(clients, kc) 24 | } 25 | 26 | uc, err := wguser.New() 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | clients = append(clients, uc) 32 | return clients, nil 33 | } 34 | -------------------------------------------------------------------------------- /os_openbsd.go: -------------------------------------------------------------------------------- 1 | //go:build openbsd 2 | // +build openbsd 3 | 4 | package wgctrl 5 | 6 | import ( 7 | "golang.zx2c4.com/wireguard/wgctrl/internal/wginternal" 8 | "golang.zx2c4.com/wireguard/wgctrl/internal/wgopenbsd" 9 | "golang.zx2c4.com/wireguard/wgctrl/internal/wguser" 10 | ) 11 | 12 | // newClients configures wginternal.Clients for OpenBSD systems. 13 | func newClients() ([]wginternal.Client, error) { 14 | var clients []wginternal.Client 15 | 16 | // OpenBSD has an in-kernel WireGuard implementation. Determine if it is 17 | // available and make use of it if so. 18 | kc, ok, err := wgopenbsd.New() 19 | if err != nil { 20 | return nil, err 21 | } 22 | if ok { 23 | clients = append(clients, kc) 24 | } 25 | 26 | uc, err := wguser.New() 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | clients = append(clients, uc) 32 | return clients, nil 33 | } 34 | -------------------------------------------------------------------------------- /.builds/openbsd.yml: -------------------------------------------------------------------------------- 1 | image: openbsd/latest 2 | packages: 3 | - bash 4 | - go 5 | sources: 6 | - https://github.com/WireGuard/wgctrl-go 7 | environment: 8 | GO111MODULE: "on" 9 | GOBIN: "/home/build/go/bin" 10 | tasks: 11 | - setup-wireguard: | 12 | ./wgctrl-go/.cibuild.sh 13 | - build: | 14 | go version 15 | go install honnef.co/go/tools/cmd/staticcheck@latest 16 | cd wgctrl-go/ 17 | go vet ./... 18 | $GOBIN/staticcheck ./... 19 | # The race detector is not supported on OpenBSD. 20 | go test -v ./... 21 | # 32-bit sanity checking for different kernel structure sizes. 22 | GOARCH=386 go build ./... 23 | go test -c . 24 | doas bash -c 'WGCTRL_INTEGRATION=yesreallydoit ./wgctrl.test -test.v -test.run TestIntegration' 25 | # Use wireguard-go for additional testing. 26 | doas /usr/local/bin/wireguard-go tun0 27 | -------------------------------------------------------------------------------- /.github/workflows/static-analysis.yml: -------------------------------------------------------------------------------- 1 | name: Static Analysis 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | pull_request: 8 | branches: 9 | - '*' 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | matrix: 15 | go-version: ["1.20"] 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Set up Go 20 | uses: actions/setup-go@v2 21 | with: 22 | go-version: ${{ matrix.go-version }} 23 | id: go 24 | 25 | - name: Check out code into the Go module directory 26 | uses: actions/checkout@v2 27 | 28 | - name: Install staticcheck 29 | run: go install honnef.co/go/tools/cmd/staticcheck@HEAD 30 | 31 | - name: Print staticcheck version 32 | run: staticcheck -version 33 | 34 | - name: Run staticcheck 35 | run: staticcheck ./... 36 | 37 | - name: Run go vet 38 | run: go vet ./... 39 | -------------------------------------------------------------------------------- /internal/wgfreebsd/internal/nv/nvlist_test.go: -------------------------------------------------------------------------------- 1 | //go:build freebsd 2 | // +build freebsd 3 | 4 | package nv_test 5 | 6 | import ( 7 | "fmt" 8 | "testing" 9 | "unsafe" 10 | 11 | "golang.zx2c4.com/wireguard/wgctrl/internal/wgfreebsd/internal/nv" 12 | ) 13 | 14 | func TestMarshaling(t *testing.T) { 15 | m1 := nv.List{ 16 | "number": uint64(0x1234), 17 | "boolean": true, 18 | "binary": []byte{0xA, 0xB, 0xC, 0xD}, 19 | "array_of_nvlists": []nv.List{ 20 | { 21 | "a": uint64(1), 22 | }, 23 | { 24 | "b": uint64(2), 25 | }, 26 | }, 27 | } 28 | 29 | buf, sz, err := nv.Marshal(m1) 30 | if err != nil { 31 | t.Fatalf("Failed to marshal: %s", err) 32 | } 33 | 34 | m2 := nv.List{} 35 | buf2 := unsafe.Slice(buf, sz) 36 | 37 | err = nv.Unmarshal(buf2, m2) 38 | if err != nil { 39 | t.Fatalf("Failed to marshal: %s", err) 40 | } 41 | 42 | if fmt.Sprint(m1) != fmt.Sprint(m2) { 43 | t.Fatalf("unequal: %+#v != %+#v", m1, m2) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | The `wgctrl` project makes use of the [GitHub Flow](https://guides.github.com/introduction/flow/) 4 | for contributions. 5 | 6 | If you'd like to contribute to the project, please 7 | [open an issue](https://github.com/WireGuard/wgctrl-go/issues/new) or find an 8 | [existing issue](https://github.com/WireGuard/wgctrl-go/issues) that you'd like 9 | to take on. This ensures that efforts are not duplicated, and that a new feature 10 | aligns with the focus of the rest of the repository. 11 | 12 | Once your suggestion has been submitted and discussed, please be sure that your 13 | code meets the following criteria: 14 | 15 | - code is completely `gofmt`'d 16 | - new features or codepaths have appropriate test coverage 17 | - `go test ./...` passes 18 | - `go vet ./...` passes 19 | - `staticcheck ./...` passes 20 | - `golint ./...` returns no warnings, including documentation comment warnings 21 | 22 | Finally, submit a pull request for review! 23 | -------------------------------------------------------------------------------- /.github/workflows/linux-integration-test.yml: -------------------------------------------------------------------------------- 1 | name: Linux Integration Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | pull_request: 8 | branches: 9 | - '*' 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | matrix: 15 | go-version: ["1.20"] 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Set up Go 20 | uses: actions/setup-go@v2 21 | with: 22 | go-version: ${{ matrix.go-version }} 23 | id: go 24 | 25 | - name: Check out code into the Go module directory 26 | uses: actions/checkout@v2 27 | 28 | - name: Set up integration test WireGuard interfaces 29 | run: ./.cibuild.sh 30 | 31 | - name: Start wireguard-go userspace device 32 | run: sudo wireguard-go wguser0 33 | 34 | - name: Build integration test binary 35 | run: go test -c -race . 36 | 37 | - name: Run integration tests 38 | run: sudo WGCTRL_INTEGRATION=yesreallydoit ./wgctrl.test -test.v -test.run TestIntegration 39 | -------------------------------------------------------------------------------- /internal/wgfreebsd/internal/wgh/generate.sh: -------------------------------------------------------------------------------- 1 | #/bin/sh 2 | 3 | set -x 4 | 5 | # Fix up generated code. 6 | gofix() 7 | { 8 | IN=$1 9 | OUT=$2 10 | 11 | # Change types that are a nuisance to deal with in Go, use byte for 12 | # consistency, and produce gofmt'd output. 13 | sed 's/]u*int8/]byte/g' $1 | gofmt -s > $2 14 | } 15 | 16 | echo -e "//+build freebsd,amd64\n" > /tmp/wgamd64.go 17 | GOARCH=amd64 go tool cgo -godefs defs.go >> /tmp/wgamd64.go 18 | 19 | echo -e "//+build freebsd,386\n" > /tmp/wg386.go 20 | GOARCH=386 go tool cgo -godefs defs.go >> /tmp/wg386.go 21 | 22 | echo -e "//+build freebsd,arm64\n" > /tmp/wgarm64.go 23 | GOARCH=arm64 go tool cgo -godefs defs.go >> /tmp/wgarm64.go 24 | 25 | echo -e "//+build freebsd,arm\n" > /tmp/wgarm.go 26 | GOARCH=arm go tool cgo -godefs defs.go >> /tmp/wgarm.go 27 | 28 | gofix /tmp/wgamd64.go defs_freebsd_amd64.go 29 | gofix /tmp/wg386.go defs_freebsd_386.go 30 | gofix /tmp/wgarm64.go defs_freebsd_arm64.go 31 | gofix /tmp/wgarm.go defs_freebsd_arm.go 32 | 33 | rm -rf _obj/ /tmp/wg*.go 34 | -------------------------------------------------------------------------------- /internal/wgopenbsd/internal/wgh/generate.sh: -------------------------------------------------------------------------------- 1 | #/bin/sh 2 | 3 | set -x 4 | 5 | # Fix up generated code. 6 | gofix() 7 | { 8 | IN=$1 9 | OUT=$2 10 | 11 | # Change types that are a nuisance to deal with in Go, use byte for 12 | # consistency, and produce gofmt'd output. 13 | sed 's/]u*int8/]byte/g' $1 | gofmt -s > $2 14 | } 15 | 16 | echo -e "//+build openbsd,amd64\n" > /tmp/wgamd64.go 17 | GOARCH=amd64 go tool cgo -godefs defs.go >> /tmp/wgamd64.go 18 | 19 | echo -e "//+build openbsd,386\n" > /tmp/wg386.go 20 | GOARCH=386 go tool cgo -godefs defs.go >> /tmp/wg386.go 21 | 22 | echo -e "//+build openbsd,arm64\n" > /tmp/wgarm64.go 23 | GOARCH=arm64 go tool cgo -godefs defs.go >> /tmp/wgarm64.go 24 | 25 | echo -e "//+build openbsd,arm\n" > /tmp/wgarm.go 26 | GOARCH=arm go tool cgo -godefs defs.go >> /tmp/wgarm.go 27 | 28 | gofix /tmp/wgamd64.go defs_openbsd_amd64.go 29 | gofix /tmp/wg386.go defs_openbsd_386.go 30 | gofix /tmp/wgarm64.go defs_openbsd_arm64.go 31 | gofix /tmp/wgarm.go defs_openbsd_arm.go 32 | 33 | rm -rf _obj/ /tmp/wg*.go 34 | -------------------------------------------------------------------------------- /os_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package wgctrl 5 | 6 | import ( 7 | "golang.zx2c4.com/wireguard/wgctrl/internal/wginternal" 8 | "golang.zx2c4.com/wireguard/wgctrl/internal/wglinux" 9 | "golang.zx2c4.com/wireguard/wgctrl/internal/wguser" 10 | ) 11 | 12 | // newClients configures wginternal.Clients for Linux systems. 13 | func newClients() ([]wginternal.Client, error) { 14 | var clients []wginternal.Client 15 | 16 | // Linux has an in-kernel WireGuard implementation. Determine if it is 17 | // available and make use of it if so. 18 | kc, ok, err := wglinux.New() 19 | if err != nil { 20 | return nil, err 21 | } 22 | if ok { 23 | clients = append(clients, kc) 24 | } 25 | 26 | // Although it isn't recommended to use userspace implementations on Linux, 27 | // it can be used. We make use of it in integration tests as well. 28 | uc, err := wguser.New() 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | // Kernel devices seem to appear first in wg(8). 34 | clients = append(clients, uc) 35 | return clients, nil 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (C) 2018-2022 Matt Layher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /.cibuild.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | set -x 4 | 5 | # !! This script is meant for use in CI build use only !! 6 | 7 | KERNEL=$(uname -s) 8 | 9 | # Use doas in place of sudo for OpenBSD. 10 | SUDO="sudo" 11 | if [ "${KERNEL}" == "OpenBSD" ]; then 12 | SUDO="doas" 13 | 14 | # Configure a WireGuard interface. 15 | doas ifconfig wg0 create 16 | doas ifconfig wg0 up 17 | fi 18 | 19 | if [ "${KERNEL}" == "FreeBSD" ]; then 20 | # Configure a WireGuard interface. 21 | sudo ifconfig wg create name wg0 22 | sudo ifconfig wg0 up 23 | fi 24 | 25 | if [ "${KERNEL}" == "Linux" ]; then 26 | # Configure a WireGuard interface. 27 | sudo ip link add wg0 type wireguard 28 | sudo ip link set up wg0 29 | fi 30 | 31 | # Set up wireguard-go on all OSes. 32 | git clone https://git.zx2c4.com/wireguard-go 33 | cd wireguard-go 34 | 35 | if [ "${KERNEL}" == "Linux" ]; then 36 | # Bypass Linux compilation restriction. 37 | make 38 | else 39 | # Build directly to avoid Makefile. 40 | go build -o wireguard-go 41 | fi 42 | 43 | ${SUDO} mv ./wireguard-go /usr/local/bin/wireguard-go 44 | cd .. 45 | ${SUDO} rm -rf ./wireguard-go 46 | -------------------------------------------------------------------------------- /internal/wguser/conn_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package wguser 5 | 6 | import ( 7 | "errors" 8 | "io/fs" 9 | "net" 10 | "os" 11 | "path/filepath" 12 | ) 13 | 14 | // dial is the default implementation of Client.dial. 15 | func dial(device string) (net.Conn, error) { 16 | return net.Dial("unix", device) 17 | } 18 | 19 | // find is the default implementation of Client.find. 20 | func find() ([]string, error) { 21 | return findUNIXSockets([]string{ 22 | // It seems that /var/run is a common location between Linux and the 23 | // BSDs, even though it's a symlink on Linux. 24 | "/var/run/wireguard", 25 | }) 26 | } 27 | 28 | // findUNIXSockets looks for UNIX socket files in the specified directories. 29 | func findUNIXSockets(dirs []string) ([]string, error) { 30 | var socks []string 31 | for _, d := range dirs { 32 | files, err := os.ReadDir(d) 33 | if err != nil { 34 | if errors.Is(err, os.ErrNotExist) { 35 | continue 36 | } 37 | 38 | return nil, err 39 | } 40 | 41 | for _, f := range files { 42 | if f.Type()&fs.ModeSocket == 0 { 43 | continue 44 | } 45 | 46 | socks = append(socks, filepath.Join(d, f.Name())) 47 | } 48 | } 49 | 50 | return socks, nil 51 | } 52 | -------------------------------------------------------------------------------- /internal/wgfreebsd/internal/wgh/defs.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | // +build ignore 3 | 4 | // TODO(mdlayher): attempt to integrate into x/sys/unix infrastructure. 5 | 6 | package wgh 7 | 8 | /* 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | struct wg_data_io { 16 | char wgd_name[IFNAMSIZ]; 17 | void *wgd_data; 18 | size_t wgd_size; 19 | }; 20 | 21 | // This is a copy of ifgroupreq but the union's *ifg_req variant is broken out 22 | // into an explicit field, and the other variant is omitted and replaced with 23 | // struct padding to the expected size. 24 | #undef ifgr_groups 25 | struct go_ifgroupreq { 26 | char ifgr_name[IFNAMSIZ]; 27 | u_int ifgr_len; 28 | char ifgr_pad1[-1 * (4 - sizeof(void*))]; 29 | struct ifg_req *ifgr_groups; 30 | char ifgr_pad2[16 - sizeof(void*)]; 31 | }; 32 | 33 | #define SIOCSWG _IOWR('i', 210, struct wg_data_io) 34 | #define SIOCGWG _IOWR('i', 211, struct wg_data_io) 35 | */ 36 | import "C" 37 | 38 | // Interface group types and constants. 39 | 40 | const ( 41 | SizeofIfgreq = C.sizeof_struct_ifg_req 42 | 43 | SIOCGWG = C.SIOCGWG 44 | SIOCSWG = C.SIOCSWG 45 | ) 46 | 47 | type Ifgroupreq C.struct_go_ifgroupreq 48 | 49 | type Ifgreq C.struct_ifg_req 50 | 51 | type WGDataIO C.struct_wg_data_io 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wgctrl [![Test Status](https://github.com/WireGuard/wgctrl-go/workflows/Linux%20Test/badge.svg)](https://github.com/WireGuard/wgctrl-go/actions) [![Go Reference](https://pkg.go.dev/badge/golang.zx2c4.com/wireguard/wgctrl.svg)](https://pkg.go.dev/golang.zx2c4.com/wireguard/wgctrl) [![Go Report Card](https://goreportcard.com/badge/golang.zx2c4.com/wireguard/wgctrl)](https://goreportcard.com/report/golang.zx2c4.com/wireguard/wgctrl) 2 | 3 | 4 | Package `wgctrl` enables control of WireGuard devices on multiple platforms. 5 | 6 | For more information on WireGuard, please see . 7 | 8 | MIT Licensed. 9 | 10 | ## Overview 11 | 12 | `wgctrl` can control multiple types of WireGuard devices, including: 13 | 14 | - Kernel module devices 15 | - Linux: via generic netlink 16 | - FreeBSD: via ioctl interface 17 | - OpenBSD: via ioctl interface (read-only) 18 | - Windows: via ioctl interface 19 | - Userspace devices via the userspace configuration protocol 20 | 21 | As new operating systems add support for in-kernel WireGuard implementations, 22 | this package should also be extended to support those native implementations. 23 | 24 | If you are aware of any efforts on this front, please 25 | [file an issue](https://github.com/WireGuard/wgctrl-go/issues/new). 26 | 27 | This package implements WireGuard configuration protocol operations, enabling 28 | the configuration of existing WireGuard devices. Operations such as creating 29 | WireGuard devices, or applying IP addresses to those devices, are out of scope 30 | for this package. 31 | -------------------------------------------------------------------------------- /internal/wgfreebsd/internal/nv/decode.go: -------------------------------------------------------------------------------- 1 | //go:build freebsd 2 | // +build freebsd 3 | 4 | package nv 5 | 6 | // #cgo LDFLAGS: -lnv 7 | // #include 8 | import "C" 9 | 10 | import ( 11 | "unsafe" 12 | ) 13 | 14 | // Unmarshal decodes a FreeBSD name-value list (nv(9)) to a Go map 15 | func Unmarshal(d []byte, out List) error { 16 | sz := C.ulong(len(d)) 17 | dp := unsafe.Pointer(&d[0]) 18 | nvl := C.nvlist_unpack(dp, sz, 0) 19 | 20 | return unmarshal(nvl, out) 21 | } 22 | 23 | func unmarshal(nvl *C.struct_nvlist, out List) error { 24 | // For debugging 25 | // C.nvlist_dump(nvl, C.int(os.Stdout.Fd())) 26 | 27 | var cookie unsafe.Pointer 28 | for { 29 | var typ C.int 30 | ckey := C.nvlist_next(nvl, &typ, &cookie) 31 | if ckey == nil { 32 | break 33 | } 34 | 35 | var sz C.size_t 36 | var value interface{} 37 | switch typ { 38 | case C.NV_TYPE_BINARY: 39 | v := C.nvlist_get_binary(nvl, ckey, &sz) 40 | value = C.GoBytes(v, C.int(sz)) 41 | 42 | case C.NV_TYPE_BOOL: 43 | value = C.nvlist_get_bool(nvl, ckey) 44 | 45 | case C.NV_TYPE_NUMBER: 46 | v := C.nvlist_get_number(nvl, ckey) 47 | value = uint64(v) 48 | 49 | case C.NV_TYPE_NVLIST_ARRAY: 50 | items := []List{} 51 | 52 | nvlSubListsBuf := C.nvlist_get_nvlist_array(nvl, ckey, &sz) 53 | nvlSubLists := unsafe.Slice(nvlSubListsBuf, sz) 54 | for _, nvlSubList := range nvlSubLists { 55 | item := map[string]interface{}{} 56 | if err := unmarshal(nvlSubList, item); err != nil { 57 | return err 58 | } 59 | 60 | items = append(items, item) 61 | } 62 | 63 | value = items 64 | } 65 | 66 | name := C.GoString(ckey) 67 | out[name] = value 68 | } 69 | 70 | return nil 71 | } 72 | -------------------------------------------------------------------------------- /internal/wgfreebsd/internal/nv/encode.go: -------------------------------------------------------------------------------- 1 | //go:build freebsd 2 | // +build freebsd 3 | 4 | package nv 5 | 6 | /* 7 | #cgo LDFLAGS: -lnv 8 | #include 9 | #include 10 | 11 | // For sizeof(*struct nvlist) 12 | typedef struct nvlist *nvlist_ptr; 13 | */ 14 | import "C" 15 | 16 | import ( 17 | "unsafe" 18 | ) 19 | 20 | // Marshal encodes a Go map to a FreeBSD name-value list (nv(9)) 21 | func Marshal(m List) (*byte, int, error) { 22 | nvl, err := marshal(m) 23 | if err != nil { 24 | return nil, -1, err 25 | } 26 | 27 | // For debugging 28 | // C.nvlist_dump(nvl, C.int(os.Stdout.Fd())) 29 | 30 | var sz C.size_t 31 | buf := C.nvlist_pack(nvl, &sz) 32 | 33 | return (*byte)(buf), int(sz), nil 34 | } 35 | 36 | func marshal(m List) (nvl *C.struct_nvlist, err error) { 37 | nvl = C.nvlist_create(0) 38 | 39 | for key, value := range m { 40 | ckey := C.CString(key) 41 | 42 | switch value := value.(type) { 43 | case bool: 44 | C.nvlist_add_bool(nvl, ckey, C.bool(value)) 45 | 46 | case uint64: 47 | C.nvlist_add_number(nvl, ckey, C.ulong(value)) 48 | 49 | case []byte: 50 | sz := len(value) 51 | ptr := C.CBytes(value) 52 | C.nvlist_add_binary(nvl, ckey, ptr, C.size_t(sz)) 53 | C.free(ptr) 54 | 55 | case []List: 56 | sz := len(value) 57 | buf := C.malloc(C.size_t(C.sizeof_nvlist_ptr * sz)) 58 | items := (*[1<<30 - 1]*C.struct_nvlist)(buf) 59 | 60 | for i, val := range value { 61 | if items[i], err = marshal(val); err != nil { 62 | C.free(unsafe.Pointer(ckey)) 63 | return nil, err 64 | } 65 | } 66 | 67 | C.nvlist_add_nvlist_array(nvl, ckey, (**C.struct_nvlist)(buf), C.size_t(sz)) 68 | C.free(buf) 69 | } 70 | 71 | C.free(unsafe.Pointer(ckey)) 72 | } 73 | 74 | return 75 | } 76 | -------------------------------------------------------------------------------- /internal/wgopenbsd/internal/wgh/defs_openbsd_386.go: -------------------------------------------------------------------------------- 1 | //go:build openbsd && 386 2 | // +build openbsd,386 3 | 4 | // Code generated by cmd/cgo -godefs; DO NOT EDIT. 5 | // cgo -godefs defs.go 6 | 7 | package wgh 8 | 9 | const ( 10 | SizeofIfgreq = 0x10 11 | ) 12 | 13 | type Ifgroupreq struct { 14 | Name [16]byte 15 | Len uint32 16 | Pad1 [0]byte 17 | Groups *Ifgreq 18 | Pad2 [12]byte 19 | } 20 | 21 | type Ifgreq struct { 22 | Ifgrqu [16]byte 23 | } 24 | 25 | type Timespec struct { 26 | Sec int64 27 | Nsec int32 28 | } 29 | 30 | type WGAIPIO struct { 31 | Af uint8 32 | Cidr int32 33 | Addr [16]byte 34 | } 35 | 36 | type WGDataIO struct { 37 | Name [16]byte 38 | Size uint32 39 | Interface *WGInterfaceIO 40 | } 41 | 42 | type WGInterfaceIO struct { 43 | Flags uint8 44 | Port uint16 45 | Rtable int32 46 | Public [32]byte 47 | Private [32]byte 48 | Peers_count uint32 49 | } 50 | 51 | type WGPeerIO struct { 52 | Flags int32 53 | Protocol_version int32 54 | Public [32]byte 55 | Psk [32]byte 56 | Pka uint16 57 | Pad_cgo_0 [2]byte 58 | Endpoint [28]byte 59 | Txbytes uint64 60 | Rxbytes uint64 61 | Last_handshake Timespec 62 | Aips_count uint32 63 | } 64 | 65 | const ( 66 | SIOCGWG = 0xc01869d3 67 | 68 | WG_INTERFACE_HAS_PUBLIC = 0x1 69 | WG_INTERFACE_HAS_PRIVATE = 0x2 70 | WG_INTERFACE_HAS_PORT = 0x4 71 | WG_INTERFACE_HAS_RTABLE = 0x8 72 | WG_INTERFACE_REPLACE_PEERS = 0x10 73 | 74 | WG_PEER_HAS_PUBLIC = 0x1 75 | WG_PEER_HAS_PSK = 0x2 76 | WG_PEER_HAS_PKA = 0x4 77 | WG_PEER_HAS_ENDPOINT = 0x8 78 | 79 | SizeofWGAIPIO = 0x18 80 | SizeofWGInterfaceIO = 0x4c 81 | SizeofWGPeerIO = 0x88 82 | ) 83 | -------------------------------------------------------------------------------- /internal/wgopenbsd/internal/wgh/defs_openbsd_amd64.go: -------------------------------------------------------------------------------- 1 | //go:build openbsd && amd64 2 | // +build openbsd,amd64 3 | 4 | // Code generated by cmd/cgo -godefs; DO NOT EDIT. 5 | // cgo -godefs defs.go 6 | 7 | package wgh 8 | 9 | const ( 10 | SizeofIfgreq = 0x10 11 | ) 12 | 13 | type Ifgroupreq struct { 14 | Name [16]byte 15 | Len uint32 16 | Pad1 [4]byte 17 | Groups *Ifgreq 18 | Pad2 [8]byte 19 | } 20 | 21 | type Ifgreq struct { 22 | Ifgrqu [16]byte 23 | } 24 | 25 | type Timespec struct { 26 | Sec int64 27 | Nsec int64 28 | } 29 | 30 | type WGAIPIO struct { 31 | Af uint8 32 | Cidr int32 33 | Addr [16]byte 34 | } 35 | 36 | type WGDataIO struct { 37 | Name [16]byte 38 | Size uint64 39 | Interface *WGInterfaceIO 40 | } 41 | 42 | type WGInterfaceIO struct { 43 | Flags uint8 44 | Port uint16 45 | Rtable int32 46 | Public [32]byte 47 | Private [32]byte 48 | Peers_count uint64 49 | } 50 | 51 | type WGPeerIO struct { 52 | Flags int32 53 | Protocol_version int32 54 | Public [32]byte 55 | Psk [32]byte 56 | Pka uint16 57 | Pad_cgo_0 [2]byte 58 | Endpoint [28]byte 59 | Txbytes uint64 60 | Rxbytes uint64 61 | Last_handshake Timespec 62 | Aips_count uint64 63 | } 64 | 65 | const ( 66 | SIOCGWG = 0xc02069d3 67 | 68 | WG_INTERFACE_HAS_PUBLIC = 0x1 69 | WG_INTERFACE_HAS_PRIVATE = 0x2 70 | WG_INTERFACE_HAS_PORT = 0x4 71 | WG_INTERFACE_HAS_RTABLE = 0x8 72 | WG_INTERFACE_REPLACE_PEERS = 0x10 73 | 74 | WG_PEER_HAS_PUBLIC = 0x1 75 | WG_PEER_HAS_PSK = 0x2 76 | WG_PEER_HAS_PKA = 0x4 77 | WG_PEER_HAS_ENDPOINT = 0x8 78 | 79 | SizeofWGAIPIO = 0x18 80 | SizeofWGInterfaceIO = 0x50 81 | SizeofWGPeerIO = 0x90 82 | ) 83 | -------------------------------------------------------------------------------- /internal/wgopenbsd/internal/wgh/defs_openbsd_arm64.go: -------------------------------------------------------------------------------- 1 | //go:build openbsd && arm64 2 | // +build openbsd,arm64 3 | 4 | // Code generated by cmd/cgo -godefs; DO NOT EDIT. 5 | // cgo -godefs defs.go 6 | 7 | package wgh 8 | 9 | const ( 10 | SizeofIfgreq = 0x10 11 | ) 12 | 13 | type Ifgroupreq struct { 14 | Name [16]byte 15 | Len uint32 16 | Pad1 [4]byte 17 | Groups *Ifgreq 18 | Pad2 [8]byte 19 | } 20 | 21 | type Ifgreq struct { 22 | Ifgrqu [16]byte 23 | } 24 | 25 | type Timespec struct { 26 | Sec int64 27 | Nsec int64 28 | } 29 | 30 | type WGAIPIO struct { 31 | Af uint8 32 | Cidr int32 33 | Addr [16]byte 34 | } 35 | 36 | type WGDataIO struct { 37 | Name [16]byte 38 | Size uint64 39 | Interface *WGInterfaceIO 40 | } 41 | 42 | type WGInterfaceIO struct { 43 | Flags uint8 44 | Port uint16 45 | Rtable int32 46 | Public [32]byte 47 | Private [32]byte 48 | Peers_count uint64 49 | } 50 | 51 | type WGPeerIO struct { 52 | Flags int32 53 | Protocol_version int32 54 | Public [32]byte 55 | Psk [32]byte 56 | Pka uint16 57 | Pad_cgo_0 [2]byte 58 | Endpoint [28]byte 59 | Txbytes uint64 60 | Rxbytes uint64 61 | Last_handshake Timespec 62 | Aips_count uint64 63 | } 64 | 65 | const ( 66 | SIOCGWG = 0xc02069d3 67 | 68 | WG_INTERFACE_HAS_PUBLIC = 0x1 69 | WG_INTERFACE_HAS_PRIVATE = 0x2 70 | WG_INTERFACE_HAS_PORT = 0x4 71 | WG_INTERFACE_HAS_RTABLE = 0x8 72 | WG_INTERFACE_REPLACE_PEERS = 0x10 73 | 74 | WG_PEER_HAS_PUBLIC = 0x1 75 | WG_PEER_HAS_PSK = 0x2 76 | WG_PEER_HAS_PKA = 0x4 77 | WG_PEER_HAS_ENDPOINT = 0x8 78 | 79 | SizeofWGAIPIO = 0x18 80 | SizeofWGInterfaceIO = 0x50 81 | SizeofWGPeerIO = 0x90 82 | ) 83 | -------------------------------------------------------------------------------- /internal/wgtest/wgtest.go: -------------------------------------------------------------------------------- 1 | package wgtest 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "net" 7 | 8 | "golang.zx2c4.com/wireguard/wgctrl/wgtypes" 9 | ) 10 | 11 | // MustCIDR converts CIDR string s into a net.IPNet or panics. 12 | func MustCIDR(s string) net.IPNet { 13 | _, cidr, err := net.ParseCIDR(s) 14 | if err != nil { 15 | panicf("wgtest: failed to parse CIDR: %v", err) 16 | } 17 | 18 | return *cidr 19 | } 20 | 21 | // MustHexKey decodes a hex string s as a key or panics. 22 | func MustHexKey(s string) wgtypes.Key { 23 | b, err := hex.DecodeString(s) 24 | if err != nil { 25 | panicf("wgtest: failed to decode hex key: %v", err) 26 | } 27 | 28 | k, err := wgtypes.NewKey(b) 29 | if err != nil { 30 | panicf("wgtest: failed to create key: %v", err) 31 | } 32 | 33 | return k 34 | } 35 | 36 | // MustPresharedKey generates a preshared key or panics. 37 | func MustPresharedKey() wgtypes.Key { 38 | k, err := wgtypes.GenerateKey() 39 | if err != nil { 40 | panicf("wgtest: failed to generate preshared key: %v", err) 41 | } 42 | 43 | return k 44 | } 45 | 46 | // MustPrivateKey generates a private key or panics. 47 | func MustPrivateKey() wgtypes.Key { 48 | k, err := wgtypes.GeneratePrivateKey() 49 | if err != nil { 50 | panicf("wgtest: failed to generate private key: %v", err) 51 | } 52 | 53 | return k 54 | } 55 | 56 | // MustPublicKey generates a public key or panics. 57 | func MustPublicKey() wgtypes.Key { 58 | return MustPrivateKey().PublicKey() 59 | } 60 | 61 | // MustUDPAddr parses s as a UDP address or panics. 62 | func MustUDPAddr(s string) *net.UDPAddr { 63 | a, err := net.ResolveUDPAddr("udp", s) 64 | if err != nil { 65 | panicf("wgtest: failed to resolve UDP address: %v", err) 66 | } 67 | 68 | return a 69 | } 70 | 71 | func panicf(format string, a ...interface{}) { 72 | panic(fmt.Sprintf(format, a...)) 73 | } 74 | -------------------------------------------------------------------------------- /internal/wgopenbsd/internal/wgh/defs_openbsd_arm.go: -------------------------------------------------------------------------------- 1 | //go:build openbsd && arm 2 | // +build openbsd,arm 3 | 4 | // Code generated by cmd/cgo -godefs; DO NOT EDIT. 5 | // cgo -godefs defs.go 6 | 7 | package wgh 8 | 9 | const ( 10 | SizeofIfgreq = 0x10 11 | ) 12 | 13 | type Ifgroupreq struct { 14 | Name [16]byte 15 | Len uint32 16 | Pad1 [0]byte 17 | Groups *Ifgreq 18 | Pad2 [12]byte 19 | } 20 | 21 | type Ifgreq struct { 22 | Ifgrqu [16]byte 23 | } 24 | 25 | type Timespec struct { 26 | Sec int64 27 | Nsec int32 28 | Pad_cgo_0 [4]byte 29 | } 30 | 31 | type WGAIPIO struct { 32 | Af uint8 33 | Cidr int32 34 | Addr [16]byte 35 | } 36 | 37 | type WGDataIO struct { 38 | Name [16]byte 39 | Size uint32 40 | Interface *WGInterfaceIO 41 | } 42 | 43 | type WGInterfaceIO struct { 44 | Flags uint8 45 | Port uint16 46 | Rtable int32 47 | Public [32]byte 48 | Private [32]byte 49 | Peers_count uint32 50 | Pad_cgo_0 [4]byte 51 | } 52 | 53 | type WGPeerIO struct { 54 | Flags int32 55 | Protocol_version int32 56 | Public [32]byte 57 | Psk [32]byte 58 | Pka uint16 59 | Pad_cgo_0 [2]byte 60 | Endpoint [28]byte 61 | Txbytes uint64 62 | Rxbytes uint64 63 | Last_handshake Timespec 64 | Aips_count uint32 65 | Aips [0]WGAIPIO 66 | Pad_cgo_1 [4]byte 67 | } 68 | 69 | const ( 70 | SIOCGWG = 0xc01869d3 71 | 72 | WG_INTERFACE_HAS_PUBLIC = 0x1 73 | WG_INTERFACE_HAS_PRIVATE = 0x2 74 | WG_INTERFACE_HAS_PORT = 0x4 75 | WG_INTERFACE_HAS_RTABLE = 0x8 76 | WG_INTERFACE_REPLACE_PEERS = 0x10 77 | 78 | WG_PEER_HAS_PUBLIC = 0x1 79 | WG_PEER_HAS_PSK = 0x2 80 | WG_PEER_HAS_PKA = 0x4 81 | WG_PEER_HAS_ENDPOINT = 0x8 82 | 83 | SizeofWGAIPIO = 0x18 84 | SizeofWGInterfaceIO = 0x50 85 | SizeofWGPeerIO = 0x90 86 | ) 87 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 2 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 3 | github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= 4 | github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= 5 | github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= 6 | github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o= 7 | github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= 8 | github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= 9 | github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= 10 | github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= 11 | github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws= 12 | github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc= 13 | golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= 14 | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 15 | golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= 16 | golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 17 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 18 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 19 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 20 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 21 | golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4= 22 | golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA= 23 | -------------------------------------------------------------------------------- /internal/wgopenbsd/internal/wgh/defs.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | // +build ignore 3 | 4 | // TODO(mdlayher): attempt to integrate into x/sys/unix infrastructure. 5 | 6 | package wgh 7 | 8 | /* 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | // This is a copy of ifgroupreq but the union's *ifg_req variant is broken out 16 | // into an explicit field, and the other variant is omitted and replaced with 17 | // struct padding to the expected size. 18 | #undef ifgr_groups 19 | struct go_ifgroupreq { 20 | char ifgr_name[IFNAMSIZ]; 21 | u_int ifgr_len; 22 | char ifgr_pad1[-1 * (4 - sizeof(void*))]; 23 | struct ifg_req *ifgr_groups; 24 | char ifgr_pad2[16 - sizeof(void*)]; 25 | }; 26 | */ 27 | import "C" 28 | 29 | // Interface group types and constants. 30 | 31 | const ( 32 | SizeofIfgreq = C.sizeof_struct_ifg_req 33 | ) 34 | 35 | type Ifgroupreq C.struct_go_ifgroupreq 36 | 37 | type Ifgreq C.struct_ifg_req 38 | 39 | type Timespec C.struct_timespec 40 | 41 | // WireGuard types and constants. 42 | 43 | type WGAIPIO C.struct_wg_aip_io 44 | 45 | type WGDataIO C.struct_wg_data_io 46 | 47 | type WGInterfaceIO C.struct_wg_interface_io 48 | 49 | type WGPeerIO C.struct_wg_peer_io 50 | 51 | const ( 52 | SIOCGWG = C.SIOCGWG 53 | 54 | WG_INTERFACE_HAS_PUBLIC = C.WG_INTERFACE_HAS_PUBLIC 55 | WG_INTERFACE_HAS_PRIVATE = C.WG_INTERFACE_HAS_PRIVATE 56 | WG_INTERFACE_HAS_PORT = C.WG_INTERFACE_HAS_PORT 57 | WG_INTERFACE_HAS_RTABLE = C.WG_INTERFACE_HAS_RTABLE 58 | WG_INTERFACE_REPLACE_PEERS = C.WG_INTERFACE_REPLACE_PEERS 59 | 60 | WG_PEER_HAS_PUBLIC = C.WG_INTERFACE_HAS_PUBLIC 61 | WG_PEER_HAS_PSK = C.WG_PEER_HAS_PSK 62 | WG_PEER_HAS_PKA = C.WG_PEER_HAS_PKA 63 | WG_PEER_HAS_ENDPOINT = C.WG_PEER_HAS_ENDPOINT 64 | 65 | SizeofWGAIPIO = C.sizeof_struct_wg_aip_io 66 | SizeofWGInterfaceIO = C.sizeof_struct_wg_interface_io 67 | SizeofWGPeerIO = C.sizeof_struct_wg_peer_io 68 | ) 69 | -------------------------------------------------------------------------------- /cmd/wgctrl/main.go: -------------------------------------------------------------------------------- 1 | // Command wgctrl is a testing utility for interacting with WireGuard via package 2 | // wgctrl. 3 | package main 4 | 5 | import ( 6 | "flag" 7 | "fmt" 8 | "log" 9 | "net" 10 | "strings" 11 | 12 | "golang.zx2c4.com/wireguard/wgctrl" 13 | "golang.zx2c4.com/wireguard/wgctrl/wgtypes" 14 | ) 15 | 16 | func main() { 17 | flag.Parse() 18 | 19 | c, err := wgctrl.New() 20 | if err != nil { 21 | log.Fatalf("failed to open wgctrl: %v", err) 22 | } 23 | defer c.Close() 24 | 25 | var devices []*wgtypes.Device 26 | if device := flag.Arg(0); device != "" { 27 | d, err := c.Device(device) 28 | if err != nil { 29 | log.Fatalf("failed to get device %q: %v", device, err) 30 | } 31 | 32 | devices = append(devices, d) 33 | } else { 34 | devices, err = c.Devices() 35 | if err != nil { 36 | log.Fatalf("failed to get devices: %v", err) 37 | } 38 | } 39 | 40 | for _, d := range devices { 41 | printDevice(d) 42 | 43 | for _, p := range d.Peers { 44 | printPeer(p) 45 | } 46 | } 47 | } 48 | 49 | func printDevice(d *wgtypes.Device) { 50 | const f = `interface: %s (%s) 51 | public key: %s 52 | private key: (hidden) 53 | listening port: %d 54 | 55 | ` 56 | 57 | fmt.Printf( 58 | f, 59 | d.Name, 60 | d.Type.String(), 61 | d.PublicKey.String(), 62 | d.ListenPort) 63 | } 64 | 65 | func printPeer(p wgtypes.Peer) { 66 | const f = `peer: %s 67 | endpoint: %s 68 | allowed ips: %s 69 | latest handshake: %s 70 | transfer: %d B received, %d B sent 71 | 72 | ` 73 | 74 | fmt.Printf( 75 | f, 76 | p.PublicKey.String(), 77 | // TODO(mdlayher): get right endpoint with getnameinfo. 78 | p.Endpoint.String(), 79 | ipsString(p.AllowedIPs), 80 | p.LastHandshakeTime.String(), 81 | p.ReceiveBytes, 82 | p.TransmitBytes, 83 | ) 84 | } 85 | 86 | func ipsString(ipns []net.IPNet) string { 87 | ss := make([]string, 0, len(ipns)) 88 | for _, ipn := range ipns { 89 | ss = append(ss, ipn.String()) 90 | } 91 | 92 | return strings.Join(ss, ", ") 93 | } 94 | -------------------------------------------------------------------------------- /internal/wguser/conn_windows_test.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package wguser 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "net" 10 | "os" 11 | "testing" 12 | "time" 13 | 14 | "golang.org/x/sys/windows/registry" 15 | "golang.zx2c4.com/wireguard/ipc/namedpipe" 16 | ) 17 | 18 | // isWINE determines if this test is running in WINE. 19 | var isWINE = func() bool { 20 | // Reference: https://forum.winehq.org/viewtopic.php?t=4988. 21 | k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Wine`, registry.QUERY_VALUE) 22 | if err != nil { 23 | if errors.Is(err, os.ErrNotExist) { 24 | // No key; the tests don't appear to be running in WINE. 25 | return false 26 | } 27 | 28 | panicf("failed to query registry for WINE: %v", err) 29 | } 30 | defer k.Close() 31 | 32 | return true 33 | }() 34 | 35 | // testFind produces a Client.find function for integration tests. 36 | func testFind(dir string) func() ([]string, error) { 37 | return func() ([]string, error) { 38 | return findNamedPipes(dir) 39 | } 40 | } 41 | 42 | // testListen creates a userspace device listener for tests, returning the 43 | // directory where it can be found and a function to clean up its state. 44 | func testListen(t *testing.T, device string) (l net.Listener, dir string, done func()) { 45 | t.Helper() 46 | 47 | // It appears that some of the system calls required for full named pipe 48 | // tests are not implemented in WINE, so skip tests that invoke this helper 49 | // if this isn't a real Windows install. 50 | if isWINE { 51 | t.Skip("skipping, creating a userspace device does not work in WINE") 52 | } 53 | 54 | // Attempt to create a unique name and avoid collisions. 55 | dir = fmt.Sprintf(`wguser-test%d\`, time.Now().Nanosecond()) 56 | 57 | l, err := namedpipe.Listen(pipePrefix + dir + device) 58 | if err != nil { 59 | t.Fatalf("failed to create Windows named pipe: %v", err) 60 | } 61 | 62 | done = func() { 63 | _ = l.Close() 64 | } 65 | 66 | return l, dir, done 67 | } 68 | -------------------------------------------------------------------------------- /internal/wguser/conn_unix_test.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package wguser 5 | 6 | import ( 7 | "net" 8 | "os" 9 | "path/filepath" 10 | "testing" 11 | 12 | "github.com/google/go-cmp/cmp" 13 | ) 14 | 15 | func TestUNIX_findUNIXSockets(t *testing.T) { 16 | tmp, err := os.MkdirTemp(os.TempDir(), "wireguardcfg-test") 17 | if err != nil { 18 | t.Fatalf("failed to create temporary directory: %v", err) 19 | } 20 | defer os.RemoveAll(tmp) 21 | 22 | // Create a file which is not a device socket. 23 | f, err := os.CreateTemp(tmp, "notwg") 24 | if err != nil { 25 | t.Fatalf("failed to create temporary file: %v", err) 26 | } 27 | _ = f.Close() 28 | 29 | // Create a temporary UNIX socket and leave it open so it is picked up 30 | // as a socket file. 31 | path := filepath.Join(tmp, "testwg0.sock") 32 | l, err := net.Listen("unix", path) 33 | if err != nil { 34 | t.Fatalf("failed to create socket: %v", err) 35 | } 36 | defer l.Close() 37 | 38 | files, err := findUNIXSockets([]string{ 39 | tmp, 40 | // Should gracefully handle non-existent directories and files. 41 | filepath.Join(tmp, "foo"), 42 | "/not/exist", 43 | }) 44 | if err != nil { 45 | t.Fatalf("failed to find files: %v", err) 46 | } 47 | 48 | if diff := cmp.Diff([]string{path}, files); diff != "" { 49 | t.Fatalf("unexpected output files (-want +got):\n%s", diff) 50 | } 51 | } 52 | 53 | // testFind produces a Client.find function for integration tests. 54 | func testFind(dir string) func() ([]string, error) { 55 | return func() ([]string, error) { 56 | return findUNIXSockets([]string{dir}) 57 | } 58 | } 59 | 60 | // testListen creates a userspace device listener for tests, returning the 61 | // directory where it can be found and a function to clean up its state. 62 | func testListen(t *testing.T, device string) (l net.Listener, dir string, done func()) { 63 | t.Helper() 64 | 65 | tmp, err := os.MkdirTemp(os.TempDir(), "wguser-test") 66 | if err != nil { 67 | t.Fatalf("failed to create temporary directory: %v", err) 68 | } 69 | 70 | path := filepath.Join(tmp, device) 71 | path += ".sock" 72 | 73 | l, err = net.Listen("unix", path) 74 | if err != nil { 75 | t.Fatalf("failed to create UNIX socket: %v", err) 76 | } 77 | 78 | done = func() { 79 | _ = l.Close() 80 | _ = os.RemoveAll(tmp) 81 | } 82 | 83 | return l, tmp, done 84 | } 85 | -------------------------------------------------------------------------------- /internal/wguser/conn_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package wguser 5 | 6 | import ( 7 | "net" 8 | "strings" 9 | "time" 10 | 11 | "golang.org/x/sys/windows" 12 | "golang.zx2c4.com/wireguard/ipc/namedpipe" 13 | ) 14 | 15 | // Expected prefixes when dealing with named pipes. 16 | const ( 17 | pipePrefix = `\\.\pipe\` 18 | wgPrefix = `ProtectedPrefix\Administrators\WireGuard\` 19 | ) 20 | 21 | // dial is the default implementation of Client.dial. 22 | func dial(device string) (net.Conn, error) { 23 | localSystem, err := windows.CreateWellKnownSid(windows.WinLocalSystemSid) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | return (&namedpipe.DialConfig{ 29 | ExpectedOwner: localSystem, 30 | }).DialTimeout(device, time.Duration(0)) 31 | } 32 | 33 | // find is the default implementation of Client.find. 34 | func find() ([]string, error) { 35 | return findNamedPipes(wgPrefix) 36 | } 37 | 38 | // findNamedPipes looks for Windows named pipes that match the specified 39 | // search string prefix. 40 | func findNamedPipes(search string) ([]string, error) { 41 | var ( 42 | pipes []string 43 | data windows.Win32finddata 44 | ) 45 | 46 | // Thanks @zx2c4 for the tips on the appropriate Windows APIs here: 47 | // https://א.cc/dHGpnhxX/c. 48 | h, err := windows.FindFirstFile( 49 | // Append * to find all named pipes. 50 | windows.StringToUTF16Ptr(pipePrefix+"*"), 51 | &data, 52 | ) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | // FindClose is used to close file search handles instead of the typical 58 | // CloseHandle used elsewhere, see: 59 | // https://docs.microsoft.com/en-us/windows/desktop/api/fileapi/nf-fileapi-findclose. 60 | defer windows.FindClose(h) 61 | 62 | // Check the first file's name for a match, but also keep searching for 63 | // WireGuard named pipes until no more files can be iterated. 64 | for { 65 | name := windows.UTF16ToString(data.FileName[:]) 66 | if strings.HasPrefix(name, search) { 67 | // Concatenate strings directly as filepath.Join appears to break the 68 | // named pipe prefix convention. 69 | pipes = append(pipes, pipePrefix+name) 70 | } 71 | 72 | if err := windows.FindNextFile(h, &data); err != nil { 73 | if err == windows.ERROR_NO_MORE_FILES { 74 | break 75 | } 76 | 77 | return nil, err 78 | } 79 | } 80 | 81 | return pipes, nil 82 | } 83 | -------------------------------------------------------------------------------- /internal/wguser/client.go: -------------------------------------------------------------------------------- 1 | package wguser 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "golang.zx2c4.com/wireguard/wgctrl/internal/wginternal" 11 | "golang.zx2c4.com/wireguard/wgctrl/wgtypes" 12 | ) 13 | 14 | var _ wginternal.Client = &Client{} 15 | 16 | // A Client provides access to userspace WireGuard device information. 17 | type Client struct { 18 | dial func(device string) (net.Conn, error) 19 | find func() ([]string, error) 20 | } 21 | 22 | // New creates a new Client. 23 | func New() (*Client, error) { 24 | return &Client{ 25 | // Operating system-specific functions which can identify and connect 26 | // to userspace WireGuard devices. These functions can also be 27 | // overridden for tests. 28 | dial: dial, 29 | find: find, 30 | }, nil 31 | } 32 | 33 | // Close implements wginternal.Client. 34 | func (c *Client) Close() error { return nil } 35 | 36 | // Devices implements wginternal.Client. 37 | func (c *Client) Devices() ([]*wgtypes.Device, error) { 38 | devices, err := c.find() 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | wgds := make([]*wgtypes.Device, 0, len(devices)) 44 | for _, d := range devices { 45 | wgd, err := c.getDevice(d) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | wgds = append(wgds, wgd) 51 | } 52 | 53 | return wgds, nil 54 | } 55 | 56 | // Device implements wginternal.Client. 57 | func (c *Client) Device(name string) (*wgtypes.Device, error) { 58 | devices, err := c.find() 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | for _, d := range devices { 64 | if name != deviceName(d) { 65 | continue 66 | } 67 | 68 | return c.getDevice(d) 69 | } 70 | 71 | return nil, os.ErrNotExist 72 | } 73 | 74 | // ConfigureDevice implements wginternal.Client. 75 | func (c *Client) ConfigureDevice(name string, cfg wgtypes.Config) error { 76 | devices, err := c.find() 77 | if err != nil { 78 | return err 79 | } 80 | 81 | for _, d := range devices { 82 | if name != deviceName(d) { 83 | continue 84 | } 85 | 86 | return c.configureDevice(d, cfg) 87 | } 88 | 89 | return os.ErrNotExist 90 | } 91 | 92 | // deviceName infers a device name from an absolute file path with extension. 93 | func deviceName(sock string) string { 94 | return strings.TrimSuffix(filepath.Base(sock), filepath.Ext(sock)) 95 | } 96 | 97 | func panicf(format string, a ...interface{}) { 98 | panic(fmt.Sprintf(format, a...)) 99 | } 100 | -------------------------------------------------------------------------------- /internal/wgwindows/internal/ioctl/winipcfg_windows.go: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: MIT 2 | * 3 | * Copyright (C) 2017-2021 WireGuard LLC. All Rights Reserved. 4 | */ 5 | 6 | package ioctl 7 | 8 | import ( 9 | "encoding/binary" 10 | "net" 11 | "unsafe" 12 | 13 | "golang.org/x/sys/windows" 14 | ) 15 | 16 | // AddressFamily enumeration specifies protocol family and is one of the windows.AF_* constants. 17 | type AddressFamily uint16 18 | 19 | // RawSockaddrInet union contains an IPv4, an IPv6 address, or an address family. 20 | // https://docs.microsoft.com/en-us/windows/desktop/api/ws2ipdef/ns-ws2ipdef-_sockaddr_inet 21 | type RawSockaddrInet struct { 22 | Family AddressFamily 23 | data [26]byte 24 | } 25 | 26 | func ntohs(i uint16) uint16 { 27 | return binary.BigEndian.Uint16((*[2]byte)(unsafe.Pointer(&i))[:]) 28 | } 29 | 30 | func htons(i uint16) uint16 { 31 | b := make([]byte, 2) 32 | binary.BigEndian.PutUint16(b, i) 33 | return *(*uint16)(unsafe.Pointer(&b[0])) 34 | } 35 | 36 | // SetIP method sets family, address, and port to the given IPv4 or IPv6 address and port. 37 | // All other members of the structure are set to zero. 38 | func (addr *RawSockaddrInet) SetIP(ip net.IP, port uint16) error { 39 | if v4 := ip.To4(); v4 != nil { 40 | addr4 := (*windows.RawSockaddrInet4)(unsafe.Pointer(addr)) 41 | addr4.Family = windows.AF_INET 42 | copy(addr4.Addr[:], v4) 43 | addr4.Port = htons(port) 44 | for i := 0; i < 8; i++ { 45 | addr4.Zero[i] = 0 46 | } 47 | return nil 48 | } 49 | 50 | if v6 := ip.To16(); v6 != nil { 51 | addr6 := (*windows.RawSockaddrInet6)(unsafe.Pointer(addr)) 52 | addr6.Family = windows.AF_INET6 53 | addr6.Port = htons(port) 54 | addr6.Flowinfo = 0 55 | copy(addr6.Addr[:], v6) 56 | addr6.Scope_id = 0 57 | return nil 58 | } 59 | 60 | return windows.ERROR_INVALID_PARAMETER 61 | } 62 | 63 | // IP returns IPv4 or IPv6 address, or nil if the address is neither. 64 | func (addr *RawSockaddrInet) IP() net.IP { 65 | switch addr.Family { 66 | case windows.AF_INET: 67 | return (*windows.RawSockaddrInet4)(unsafe.Pointer(addr)).Addr[:] 68 | 69 | case windows.AF_INET6: 70 | return (*windows.RawSockaddrInet6)(unsafe.Pointer(addr)).Addr[:] 71 | } 72 | 73 | return nil 74 | } 75 | 76 | // Port returns the port if the address if IPv4 or IPv6, or 0 if neither. 77 | func (addr *RawSockaddrInet) Port() uint16 { 78 | switch addr.Family { 79 | case windows.AF_INET: 80 | return ntohs((*windows.RawSockaddrInet4)(unsafe.Pointer(addr)).Port) 81 | 82 | case windows.AF_INET6: 83 | return ntohs((*windows.RawSockaddrInet6)(unsafe.Pointer(addr)).Port) 84 | } 85 | 86 | return 0 87 | } 88 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package wgctrl 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | 7 | "golang.zx2c4.com/wireguard/wgctrl/internal/wginternal" 8 | "golang.zx2c4.com/wireguard/wgctrl/wgtypes" 9 | ) 10 | 11 | // Expose an identical interface to the underlying packages. 12 | var _ wginternal.Client = &Client{} 13 | 14 | // A Client provides access to WireGuard device information. 15 | type Client struct { 16 | // Seamlessly use different wginternal.Client implementations to provide an 17 | // interface similar to wg(8). 18 | cs []wginternal.Client 19 | } 20 | 21 | // New creates a new Client. 22 | func New() (*Client, error) { 23 | cs, err := newClients() 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | return &Client{ 29 | cs: cs, 30 | }, nil 31 | } 32 | 33 | // Close releases resources used by a Client. 34 | func (c *Client) Close() error { 35 | for _, wgc := range c.cs { 36 | if err := wgc.Close(); err != nil { 37 | return err 38 | } 39 | } 40 | 41 | return nil 42 | } 43 | 44 | // Devices retrieves all WireGuard devices on this system. 45 | func (c *Client) Devices() ([]*wgtypes.Device, error) { 46 | var out []*wgtypes.Device 47 | for _, wgc := range c.cs { 48 | devs, err := wgc.Devices() 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | out = append(out, devs...) 54 | } 55 | 56 | return out, nil 57 | } 58 | 59 | // Device retrieves a WireGuard device by its interface name. 60 | // 61 | // If the device specified by name does not exist or is not a WireGuard device, 62 | // an error is returned which can be checked using `errors.Is(err, os.ErrNotExist)`. 63 | func (c *Client) Device(name string) (*wgtypes.Device, error) { 64 | for _, wgc := range c.cs { 65 | d, err := wgc.Device(name) 66 | switch { 67 | case err == nil: 68 | return d, nil 69 | case errors.Is(err, os.ErrNotExist): 70 | continue 71 | default: 72 | return nil, err 73 | } 74 | } 75 | 76 | return nil, os.ErrNotExist 77 | } 78 | 79 | // ConfigureDevice configures a WireGuard device by its interface name. 80 | // 81 | // Because the zero value of some Go types may be significant to WireGuard for 82 | // Config fields, only fields which are not nil will be applied when 83 | // configuring a device. 84 | // 85 | // If the device specified by name does not exist or is not a WireGuard device, 86 | // an error is returned which can be checked using `errors.Is(err, os.ErrNotExist)`. 87 | func (c *Client) ConfigureDevice(name string, cfg wgtypes.Config) error { 88 | for _, wgc := range c.cs { 89 | err := wgc.ConfigureDevice(name, cfg) 90 | switch { 91 | case err == nil: 92 | return nil 93 | case errors.Is(err, os.ErrNotExist): 94 | continue 95 | default: 96 | return err 97 | } 98 | } 99 | 100 | return os.ErrNotExist 101 | } 102 | -------------------------------------------------------------------------------- /internal/wguser/configure.go: -------------------------------------------------------------------------------- 1 | package wguser 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "fmt" 7 | "io" 8 | "os" 9 | "strings" 10 | 11 | "golang.zx2c4.com/wireguard/wgctrl/wgtypes" 12 | ) 13 | 14 | // configureDevice configures a device specified by its path. 15 | func (c *Client) configureDevice(device string, cfg wgtypes.Config) error { 16 | conn, err := c.dial(device) 17 | if err != nil { 18 | return err 19 | } 20 | defer conn.Close() 21 | 22 | // Start with set command. 23 | var buf bytes.Buffer 24 | buf.WriteString("set=1\n") 25 | 26 | // Add any necessary configuration from cfg, then finish with an empty line. 27 | writeConfig(&buf, cfg) 28 | buf.WriteString("\n") 29 | 30 | // Apply configuration for the device and then check the error number. 31 | if _, err := io.Copy(conn, &buf); err != nil { 32 | return err 33 | } 34 | 35 | res := make([]byte, 32) 36 | n, err := conn.Read(res) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | // errno=0 indicates success, anything else returns an error number that 42 | // matches definitions from errno.h. 43 | str := strings.TrimSpace(string(res[:n])) 44 | if str != "errno=0" { 45 | // TODO(mdlayher): return actual errno on Linux? 46 | return os.NewSyscallError("read", fmt.Errorf("wguser: %s", str)) 47 | } 48 | 49 | return nil 50 | } 51 | 52 | // writeConfig writes textual configuration to w as specified by cfg. 53 | func writeConfig(w io.Writer, cfg wgtypes.Config) { 54 | if cfg.PrivateKey != nil { 55 | fmt.Fprintf(w, "private_key=%s\n", hexKey(*cfg.PrivateKey)) 56 | } 57 | 58 | if cfg.ListenPort != nil { 59 | fmt.Fprintf(w, "listen_port=%d\n", *cfg.ListenPort) 60 | } 61 | 62 | if cfg.FirewallMark != nil { 63 | fmt.Fprintf(w, "fwmark=%d\n", *cfg.FirewallMark) 64 | } 65 | 66 | if cfg.ReplacePeers { 67 | fmt.Fprintln(w, "replace_peers=true") 68 | } 69 | 70 | for _, p := range cfg.Peers { 71 | fmt.Fprintf(w, "public_key=%s\n", hexKey(p.PublicKey)) 72 | 73 | if p.Remove { 74 | fmt.Fprintln(w, "remove=true") 75 | } 76 | 77 | if p.UpdateOnly { 78 | fmt.Fprintln(w, "update_only=true") 79 | } 80 | 81 | if p.PresharedKey != nil { 82 | fmt.Fprintf(w, "preshared_key=%s\n", hexKey(*p.PresharedKey)) 83 | } 84 | 85 | if p.Endpoint != nil { 86 | fmt.Fprintf(w, "endpoint=%s\n", p.Endpoint.String()) 87 | } 88 | 89 | if p.PersistentKeepaliveInterval != nil { 90 | fmt.Fprintf(w, "persistent_keepalive_interval=%d\n", int(p.PersistentKeepaliveInterval.Seconds())) 91 | } 92 | 93 | if p.ReplaceAllowedIPs { 94 | fmt.Fprintln(w, "replace_allowed_ips=true") 95 | } 96 | 97 | for _, ip := range p.AllowedIPs { 98 | fmt.Fprintf(w, "allowed_ip=%s\n", ip.String()) 99 | } 100 | } 101 | } 102 | 103 | // hexKey encodes a wgtypes.Key into a hexadecimal string. 104 | func hexKey(k wgtypes.Key) string { 105 | return hex.EncodeToString(k[:]) 106 | } 107 | -------------------------------------------------------------------------------- /wgtypes/types_test.go: -------------------------------------------------------------------------------- 1 | package wgtypes_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/google/go-cmp/cmp" 9 | "golang.org/x/crypto/curve25519" 10 | "golang.zx2c4.com/wireguard/wgctrl/wgtypes" 11 | ) 12 | 13 | func TestPreparedKeys(t *testing.T) { 14 | // Keys generated via "wg genkey" and "wg pubkey" for comparison 15 | // with this Go implementation. 16 | const ( 17 | private = "GHuMwljFfqd2a7cs6BaUOmHflK23zME8VNvC5B37S3k=" 18 | public = "aPxGwq8zERHQ3Q1cOZFdJ+cvJX5Ka4mLN38AyYKYF10=" 19 | ) 20 | 21 | priv, err := wgtypes.ParseKey(private) 22 | if err != nil { 23 | t.Fatalf("failed to parse private key: %v", err) 24 | } 25 | 26 | if diff := cmp.Diff(private, priv.String()); diff != "" { 27 | t.Fatalf("unexpected private key (-want +got):\n%s", diff) 28 | } 29 | 30 | pub := priv.PublicKey() 31 | if diff := cmp.Diff(public, pub.String()); diff != "" { 32 | t.Fatalf("unexpected public key (-want +got):\n%s", diff) 33 | } 34 | } 35 | 36 | func TestKeyExchange(t *testing.T) { 37 | privA, pubA := mustKeyPair() 38 | privB, pubB := mustKeyPair() 39 | 40 | // Perform ECDH key exchange: https://cr.yp.to/ecdh.html. 41 | sharedA, err := curve25519.X25519(privA[:], pubB[:]) 42 | if err != nil { 43 | t.Fatalf("failed to perform X25519 A: %v", err) 44 | } 45 | sharedB, err := curve25519.X25519(privB[:], pubA[:]) 46 | if err != nil { 47 | t.Fatalf("failed to perform X25519 B: %v", err) 48 | } 49 | 50 | if diff := cmp.Diff(sharedA, sharedB); diff != "" { 51 | t.Fatalf("unexpected shared secret (-want +got):\n%s", diff) 52 | } 53 | } 54 | 55 | func TestBadKeys(t *testing.T) { 56 | // Adapt to fit the signature used in the test table. 57 | parseKey := func(b []byte) (wgtypes.Key, error) { 58 | return wgtypes.ParseKey(string(b)) 59 | } 60 | 61 | tests := []struct { 62 | name string 63 | b []byte 64 | fn func(b []byte) (wgtypes.Key, error) 65 | }{ 66 | { 67 | name: "bad base64", 68 | b: []byte("xxx"), 69 | fn: parseKey, 70 | }, 71 | { 72 | name: "short base64", 73 | b: []byte("aGVsbG8="), 74 | fn: parseKey, 75 | }, 76 | { 77 | name: "short key", 78 | b: []byte("xxx"), 79 | fn: wgtypes.NewKey, 80 | }, 81 | { 82 | name: "long base64", 83 | b: []byte("ZGVhZGJlZWZkZWFkYmVlZmRlYWRiZWVmZGVhZGJlZWZkZWFkYmVlZg=="), 84 | fn: parseKey, 85 | }, 86 | { 87 | name: "long bytes", 88 | b: bytes.Repeat([]byte{0xff}, 40), 89 | fn: wgtypes.NewKey, 90 | }, 91 | } 92 | 93 | for _, tt := range tests { 94 | t.Run(tt.name, func(t *testing.T) { 95 | _, err := tt.fn(tt.b) 96 | if err == nil { 97 | t.Fatal("expected an error, but none occurred") 98 | } 99 | 100 | t.Logf("OK error: %v", err) 101 | }) 102 | } 103 | } 104 | 105 | func mustKeyPair() (private, public *[32]byte) { 106 | priv, err := wgtypes.GeneratePrivateKey() 107 | if err != nil { 108 | panicf("failed to generate private key: %v", err) 109 | } 110 | 111 | return keyPtr(priv), keyPtr(priv.PublicKey()) 112 | } 113 | 114 | func keyPtr(k wgtypes.Key) *[32]byte { 115 | b32 := [32]byte(k) 116 | return &b32 117 | } 118 | 119 | func panicf(format string, a ...interface{}) { 120 | panic(fmt.Sprintf(format, a...)) 121 | } 122 | -------------------------------------------------------------------------------- /internal/wguser/client_test.go: -------------------------------------------------------------------------------- 1 | package wguser 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "strings" 7 | "sync" 8 | "testing" 9 | "time" 10 | 11 | "github.com/google/go-cmp/cmp" 12 | "golang.zx2c4.com/wireguard/wgctrl/wgtypes" 13 | ) 14 | 15 | // A known device name used throughout unit and integration tests. 16 | const testDevice = "wgtest0" 17 | 18 | func TestClientDevice(t *testing.T) { 19 | tests := []struct { 20 | name string 21 | device string 22 | exists bool 23 | d *wgtypes.Device 24 | }{ 25 | { 26 | name: "not found", 27 | device: "wg1", 28 | }, 29 | { 30 | name: "ok", 31 | device: testDevice, 32 | exists: true, 33 | d: &wgtypes.Device{ 34 | Name: testDevice, 35 | Type: wgtypes.Userspace, 36 | PublicKey: wgtypes.Key{0x2f, 0xe5, 0x7d, 0xa3, 0x47, 0xcd, 0x62, 0x43, 0x15, 0x28, 0xda, 0xac, 0x5f, 0xbb, 0x29, 0x7, 0x30, 0xff, 0xf6, 0x84, 0xaf, 0xc4, 0xcf, 0xc2, 0xed, 0x90, 0x99, 0x5f, 0x58, 0xcb, 0x3b, 0x74}, 37 | }, 38 | }, 39 | } 40 | 41 | for _, tt := range tests { 42 | t.Run(tt.name, func(t *testing.T) { 43 | c, done := testClient(t, nil) 44 | defer done() 45 | 46 | dev, err := c.Device(tt.device) 47 | if err != nil { 48 | if !tt.exists && errors.Is(err, os.ErrNotExist) { 49 | return 50 | } 51 | 52 | t.Fatalf("failed to get device: %v", err) 53 | } 54 | 55 | if diff := cmp.Diff(tt.d, dev); diff != "" { 56 | t.Fatalf("unexpected Device (-want +got):\n%s", diff) 57 | } 58 | }) 59 | } 60 | } 61 | 62 | func testClient(t *testing.T, res []byte) (*Client, func() []byte) { 63 | t.Helper() 64 | 65 | // Create a temporary userspace device listener backed by a UNIX socket or 66 | // Windows named pipe. 67 | l, dir, done := testListen(t, testDevice) 68 | t.Logf("userspace device: %s", l.Addr()) 69 | 70 | // When no response is specified, send "OK". 71 | if res == nil { 72 | res = []byte("errno=0\n\n") 73 | } 74 | 75 | // Request is passed to the caller on return from done func. 76 | var mu sync.Mutex 77 | req := make([]byte, 4096) 78 | 79 | var wg sync.WaitGroup 80 | wg.Add(1) 81 | 82 | go func() { 83 | defer wg.Done() 84 | 85 | c, err := l.Accept() 86 | if err != nil { 87 | if strings.Contains(err.Error(), "use of closed") { 88 | return 89 | } 90 | 91 | panicf("failed to accept connection: %v", err) 92 | } 93 | defer c.Close() 94 | 95 | mu.Lock() 96 | defer mu.Unlock() 97 | 98 | // Pass request to the caller. 99 | n, err := c.Read(req) 100 | if err != nil { 101 | panicf("failed to read request: %v", err) 102 | } 103 | req = req[:n] 104 | 105 | if _, err := c.Write(res); err != nil { 106 | panicf("failed to write response: %v", err) 107 | } 108 | }() 109 | 110 | c := &Client{ 111 | // Point the Client at our temporary userspace device listener. 112 | find: testFind(dir), 113 | dial: dial, 114 | } 115 | 116 | return c, func() []byte { 117 | mu.Lock() 118 | defer mu.Unlock() 119 | 120 | _ = c.Close() 121 | done() 122 | wg.Wait() 123 | 124 | return req 125 | } 126 | } 127 | 128 | func durPtr(d time.Duration) *time.Duration { return &d } 129 | func keyPtr(k wgtypes.Key) *wgtypes.Key { return &k } 130 | func intPtr(v int) *int { return &v } 131 | -------------------------------------------------------------------------------- /internal/wgwindows/internal/ioctl/configuration_windows.go: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: MIT 2 | * 3 | * Copyright (C) 2017-2021 WireGuard LLC. All Rights Reserved. 4 | */ 5 | 6 | package ioctl 7 | 8 | import "unsafe" 9 | 10 | const ( 11 | IoctlGet = 0xb098c506 12 | IoctlSet = 0xb098c509 13 | ) 14 | 15 | type AllowedIP struct { 16 | Address [16]byte 17 | AddressFamily AddressFamily 18 | Cidr uint8 19 | _ [4]byte 20 | } 21 | 22 | type PeerFlag uint32 23 | 24 | const ( 25 | PeerHasPublicKey PeerFlag = 1 << 0 26 | PeerHasPresharedKey PeerFlag = 1 << 1 27 | PeerHasPersistentKeepalive PeerFlag = 1 << 2 28 | PeerHasEndpoint PeerFlag = 1 << 3 29 | PeerHasProtocolVersion PeerFlag = 1 << 4 30 | PeerReplaceAllowedIPs PeerFlag = 1 << 5 31 | PeerRemove PeerFlag = 1 << 6 32 | PeerUpdateOnly PeerFlag = 1 << 7 33 | ) 34 | 35 | type Peer struct { 36 | Flags PeerFlag 37 | ProtocolVersion uint32 38 | PublicKey [32]byte 39 | PresharedKey [32]byte 40 | PersistentKeepalive uint16 41 | _ uint16 42 | Endpoint RawSockaddrInet 43 | TxBytes uint64 44 | RxBytes uint64 45 | LastHandshake uint64 46 | AllowedIPsCount uint32 47 | _ [4]byte 48 | } 49 | 50 | type InterfaceFlag uint32 51 | 52 | const ( 53 | InterfaceHasPublicKey InterfaceFlag = 1 << 0 54 | InterfaceHasPrivateKey InterfaceFlag = 1 << 1 55 | InterfaceHasListenPort InterfaceFlag = 1 << 2 56 | InterfaceReplacePeers InterfaceFlag = 1 << 3 57 | ) 58 | 59 | type Interface struct { 60 | Flags InterfaceFlag 61 | ListenPort uint16 62 | PrivateKey [32]byte 63 | PublicKey [32]byte 64 | PeerCount uint32 65 | _ [4]byte 66 | } 67 | 68 | func (interfaze *Interface) FirstPeer() *Peer { 69 | return (*Peer)(unsafe.Pointer(uintptr(unsafe.Pointer(interfaze)) + unsafe.Sizeof(*interfaze))) 70 | } 71 | 72 | func (peer *Peer) NextPeer() *Peer { 73 | return (*Peer)(unsafe.Pointer(uintptr(unsafe.Pointer(peer)) + unsafe.Sizeof(*peer) + uintptr(peer.AllowedIPsCount)*unsafe.Sizeof(AllowedIP{}))) 74 | } 75 | 76 | func (peer *Peer) FirstAllowedIP() *AllowedIP { 77 | return (*AllowedIP)(unsafe.Pointer(uintptr(unsafe.Pointer(peer)) + unsafe.Sizeof(*peer))) 78 | } 79 | 80 | func (allowedIP *AllowedIP) NextAllowedIP() *AllowedIP { 81 | return (*AllowedIP)(unsafe.Pointer(uintptr(unsafe.Pointer(allowedIP)) + unsafe.Sizeof(*allowedIP))) 82 | } 83 | 84 | type ConfigBuilder struct { 85 | buffer []byte 86 | } 87 | 88 | func (builder *ConfigBuilder) Preallocate(size uint32) { 89 | if builder.buffer == nil { 90 | builder.buffer = make([]byte, 0, size) 91 | } 92 | } 93 | 94 | func (builder *ConfigBuilder) AppendInterface(interfaze *Interface) { 95 | var newBytes []byte 96 | unsafeSlice(unsafe.Pointer(&newBytes), unsafe.Pointer(interfaze), int(unsafe.Sizeof(*interfaze))) 97 | builder.buffer = append(builder.buffer, newBytes...) 98 | } 99 | 100 | func (builder *ConfigBuilder) AppendPeer(peer *Peer) { 101 | var newBytes []byte 102 | unsafeSlice(unsafe.Pointer(&newBytes), unsafe.Pointer(peer), int(unsafe.Sizeof(*peer))) 103 | builder.buffer = append(builder.buffer, newBytes...) 104 | } 105 | 106 | func (builder *ConfigBuilder) AppendAllowedIP(allowedIP *AllowedIP) { 107 | var newBytes []byte 108 | unsafeSlice(unsafe.Pointer(&newBytes), unsafe.Pointer(allowedIP), int(unsafe.Sizeof(*allowedIP))) 109 | builder.buffer = append(builder.buffer, newBytes...) 110 | } 111 | 112 | func (builder *ConfigBuilder) Interface() (*Interface, uint32) { 113 | if builder.buffer == nil { 114 | return nil, 0 115 | } 116 | return (*Interface)(unsafe.Pointer(&builder.buffer[0])), uint32(len(builder.buffer)) 117 | } 118 | 119 | // unsafeSlice updates the slice slicePtr to be a slice 120 | // referencing the provided data with its length & capacity set to 121 | // lenCap. 122 | // 123 | // TODO: whenGo 1.17 is the minimum supported version, 124 | // update callers to use unsafe.Slice instead of this. 125 | func unsafeSlice(slicePtr, data unsafe.Pointer, lenCap int) { 126 | type sliceHeader struct { 127 | Data unsafe.Pointer 128 | Len int 129 | Cap int 130 | } 131 | h := (*sliceHeader)(slicePtr) 132 | h.Data = data 133 | h.Len = lenCap 134 | h.Cap = lenCap 135 | } 136 | -------------------------------------------------------------------------------- /internal/wguser/configure_test.go: -------------------------------------------------------------------------------- 1 | package wguser 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "os" 7 | "testing" 8 | "time" 9 | 10 | "golang.zx2c4.com/wireguard/wgctrl/internal/wgtest" 11 | "golang.zx2c4.com/wireguard/wgctrl/wgtypes" 12 | ) 13 | 14 | // Example string source (with some slight modifications to use all fields): 15 | // https://www.wireguard.com/xplatform/#cross-platform-userspace-implementation. 16 | const okSet = `set=1 17 | private_key=e84b5a6d2717c1003a13b431570353dbaca9146cf150c5f8575680feba52027a 18 | listen_port=12912 19 | fwmark=0 20 | replace_peers=true 21 | public_key=b85996fecc9c7f1fc6d2572a76eda11d59bcd20be8e543b15ce4bd85a8e75a33 22 | preshared_key=188515093e952f5f22e865cef3012e72f8b5f0b598ac0309d5dacce3b70fcf52 23 | endpoint=[abcd:23::33%2]:51820 24 | replace_allowed_ips=true 25 | allowed_ip=192.168.4.4/32 26 | public_key=58402e695ba1772b1cc9309755f043251ea77fdcf10fbe63989ceb7e19321376 27 | update_only=true 28 | endpoint=182.122.22.19:3233 29 | persistent_keepalive_interval=111 30 | replace_allowed_ips=true 31 | allowed_ip=192.168.4.6/32 32 | public_key=662e14fd594556f522604703340351258903b64f35553763f19426ab2a515c58 33 | endpoint=5.152.198.39:51820 34 | replace_allowed_ips=true 35 | allowed_ip=192.168.4.10/32 36 | allowed_ip=192.168.4.11/32 37 | public_key=e818b58db5274087fcc1be5dc728cf53d3b5726b4cef6b9bab8f8f8c2452c25c 38 | remove=true 39 | 40 | ` 41 | 42 | func TestClientConfigureDeviceError(t *testing.T) { 43 | tests := []struct { 44 | name string 45 | device string 46 | cfg wgtypes.Config 47 | res []byte 48 | notExist bool 49 | }{ 50 | { 51 | name: "not found", 52 | device: "wg1", 53 | notExist: true, 54 | }, 55 | { 56 | name: "bad errno", 57 | device: testDevice, 58 | res: []byte("errno=1\n\n"), 59 | }, 60 | } 61 | 62 | for _, tt := range tests { 63 | t.Run(tt.name, func(t *testing.T) { 64 | c, done := testClient(t, tt.res) 65 | defer done() 66 | 67 | err := c.ConfigureDevice(tt.device, tt.cfg) 68 | if err == nil { 69 | t.Fatal("expected an error, but none occurred") 70 | } 71 | 72 | if !tt.notExist && errors.Is(err, os.ErrNotExist) { 73 | t.Fatalf("expected other error, but got not exist: %v", err) 74 | } 75 | if tt.notExist && !errors.Is(err, os.ErrNotExist) { 76 | t.Fatalf("expected not exist error, but got: %v", err) 77 | } 78 | }) 79 | } 80 | } 81 | 82 | func TestClientConfigureDeviceOK(t *testing.T) { 83 | tests := []struct { 84 | name string 85 | cfg wgtypes.Config 86 | req string 87 | }{ 88 | { 89 | name: "ok, none", 90 | req: "set=1\n\n", 91 | }, 92 | { 93 | name: "ok, clear key", 94 | cfg: wgtypes.Config{ 95 | PrivateKey: &wgtypes.Key{}, 96 | }, 97 | req: "set=1\nprivate_key=0000000000000000000000000000000000000000000000000000000000000000\n\n", 98 | }, 99 | { 100 | name: "ok, all", 101 | cfg: wgtypes.Config{ 102 | PrivateKey: keyPtr(wgtest.MustHexKey("e84b5a6d2717c1003a13b431570353dbaca9146cf150c5f8575680feba52027a")), 103 | ListenPort: intPtr(12912), 104 | FirewallMark: intPtr(0), 105 | ReplacePeers: true, 106 | Peers: []wgtypes.PeerConfig{ 107 | { 108 | PublicKey: wgtest.MustHexKey("b85996fecc9c7f1fc6d2572a76eda11d59bcd20be8e543b15ce4bd85a8e75a33"), 109 | PresharedKey: keyPtr(wgtest.MustHexKey("188515093e952f5f22e865cef3012e72f8b5f0b598ac0309d5dacce3b70fcf52")), 110 | Endpoint: wgtest.MustUDPAddr("[abcd:23::33%2]:51820"), 111 | ReplaceAllowedIPs: true, 112 | AllowedIPs: []net.IPNet{ 113 | wgtest.MustCIDR("192.168.4.4/32"), 114 | }, 115 | }, 116 | { 117 | PublicKey: wgtest.MustHexKey("58402e695ba1772b1cc9309755f043251ea77fdcf10fbe63989ceb7e19321376"), 118 | UpdateOnly: true, 119 | Endpoint: wgtest.MustUDPAddr("182.122.22.19:3233"), 120 | PersistentKeepaliveInterval: durPtr(111 * time.Second), 121 | ReplaceAllowedIPs: true, 122 | AllowedIPs: []net.IPNet{ 123 | wgtest.MustCIDR("192.168.4.6/32"), 124 | }, 125 | }, 126 | { 127 | PublicKey: wgtest.MustHexKey("662e14fd594556f522604703340351258903b64f35553763f19426ab2a515c58"), 128 | Endpoint: wgtest.MustUDPAddr("5.152.198.39:51820"), 129 | ReplaceAllowedIPs: true, 130 | AllowedIPs: []net.IPNet{ 131 | wgtest.MustCIDR("192.168.4.10/32"), 132 | wgtest.MustCIDR("192.168.4.11/32"), 133 | }, 134 | }, 135 | { 136 | PublicKey: wgtest.MustHexKey("e818b58db5274087fcc1be5dc728cf53d3b5726b4cef6b9bab8f8f8c2452c25c"), 137 | Remove: true, 138 | }, 139 | }, 140 | }, 141 | req: okSet, 142 | }, 143 | } 144 | 145 | for _, tt := range tests { 146 | t.Run(tt.name, func(t *testing.T) { 147 | c, done := testClient(t, nil) 148 | 149 | if err := c.ConfigureDevice(testDevice, tt.cfg); err != nil { 150 | t.Fatalf("failed to configure device: %v", err) 151 | } 152 | 153 | req := done() 154 | 155 | if want, got := tt.req, string(req); want != got { 156 | t.Fatalf("unexpected configure request:\nwant:\n%s\ngot:\n%s", want, got) 157 | } 158 | }) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package wgctrl 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "testing" 7 | 8 | "github.com/google/go-cmp/cmp" 9 | "golang.zx2c4.com/wireguard/wgctrl/internal/wginternal" 10 | "golang.zx2c4.com/wireguard/wgctrl/wgtypes" 11 | ) 12 | 13 | var ( 14 | errFoo = errors.New("some error") 15 | 16 | okDevice = &wgtypes.Device{Name: "wg0"} 17 | 18 | cmpErrors = cmp.Comparer(func(x, y error) bool { 19 | return x.Error() == y.Error() 20 | }) 21 | ) 22 | 23 | func TestClientClose(t *testing.T) { 24 | var calls int 25 | fn := func() error { 26 | calls++ 27 | return nil 28 | } 29 | 30 | c := &Client{ 31 | cs: []wginternal.Client{ 32 | &testClient{CloseFunc: fn}, 33 | &testClient{CloseFunc: fn}, 34 | }, 35 | } 36 | 37 | if err := c.Close(); err != nil { 38 | t.Fatalf("failed to close: %v", err) 39 | } 40 | 41 | if diff := cmp.Diff(2, calls); diff != "" { 42 | t.Fatalf("unexpected number of clients closed (-want +got):\n%s", diff) 43 | } 44 | } 45 | 46 | func TestClientDevices(t *testing.T) { 47 | fn := func() ([]*wgtypes.Device, error) { 48 | return []*wgtypes.Device{okDevice}, nil 49 | } 50 | 51 | c := &Client{ 52 | cs: []wginternal.Client{ 53 | // Same device retrieved twice, but we don't check uniqueness. 54 | &testClient{DevicesFunc: fn}, 55 | &testClient{DevicesFunc: fn}, 56 | }, 57 | } 58 | 59 | devices, err := c.Devices() 60 | if err != nil { 61 | t.Fatalf("failed to get devices: %v", err) 62 | } 63 | 64 | if diff := cmp.Diff(2, len(devices)); diff != "" { 65 | t.Fatalf("unexpected number of devices (-want +got):\n%s", diff) 66 | } 67 | } 68 | 69 | func TestClientDevice(t *testing.T) { 70 | type deviceFunc func(name string) (*wgtypes.Device, error) 71 | 72 | var ( 73 | notExist = func(_ string) (*wgtypes.Device, error) { 74 | return nil, os.ErrNotExist 75 | } 76 | 77 | willPanic = func(_ string) (*wgtypes.Device, error) { 78 | panic("shouldn't be called") 79 | } 80 | 81 | returnDevice = func(_ string) (*wgtypes.Device, error) { 82 | return okDevice, nil 83 | } 84 | ) 85 | 86 | tests := []struct { 87 | name string 88 | fns []deviceFunc 89 | err error 90 | }{ 91 | { 92 | name: "first error", 93 | fns: []deviceFunc{ 94 | func(_ string) (*wgtypes.Device, error) { 95 | return nil, errFoo 96 | }, 97 | willPanic, 98 | }, 99 | err: errFoo, 100 | }, 101 | { 102 | name: "not found", 103 | fns: []deviceFunc{ 104 | notExist, 105 | notExist, 106 | }, 107 | err: os.ErrNotExist, 108 | }, 109 | { 110 | name: "first not found", 111 | fns: []deviceFunc{ 112 | notExist, 113 | returnDevice, 114 | }, 115 | }, 116 | { 117 | name: "first ok", 118 | fns: []deviceFunc{ 119 | returnDevice, 120 | willPanic, 121 | }, 122 | }, 123 | } 124 | 125 | for _, tt := range tests { 126 | t.Run(tt.name, func(t *testing.T) { 127 | var cs []wginternal.Client 128 | for _, fn := range tt.fns { 129 | cs = append(cs, &testClient{ 130 | DeviceFunc: fn, 131 | }) 132 | } 133 | 134 | c := &Client{cs: cs} 135 | 136 | d, err := c.Device("") 137 | 138 | if diff := cmp.Diff(tt.err, err, cmpErrors); diff != "" { 139 | t.Fatalf("unexpected error (-want +got):\n%s", diff) 140 | } 141 | if err != nil { 142 | return 143 | } 144 | 145 | if diff := cmp.Diff(okDevice, d); diff != "" { 146 | t.Fatalf("unexpected device (-want +got):\n%s", diff) 147 | } 148 | }) 149 | } 150 | } 151 | 152 | func TestClientConfigureDevice(t *testing.T) { 153 | type configFunc func(name string, cfg wgtypes.Config) error 154 | 155 | var ( 156 | notExist = func(_ string, _ wgtypes.Config) error { 157 | return os.ErrNotExist 158 | } 159 | 160 | willPanic = func(_ string, _ wgtypes.Config) error { 161 | panic("shouldn't be called") 162 | } 163 | 164 | ok = func(_ string, _ wgtypes.Config) error { 165 | return nil 166 | } 167 | ) 168 | 169 | tests := []struct { 170 | name string 171 | fns []configFunc 172 | err error 173 | }{ 174 | { 175 | name: "first error", 176 | fns: []configFunc{ 177 | func(_ string, _ wgtypes.Config) error { 178 | return errFoo 179 | }, 180 | willPanic, 181 | }, 182 | err: errFoo, 183 | }, 184 | { 185 | name: "not found", 186 | fns: []configFunc{ 187 | notExist, 188 | notExist, 189 | }, 190 | err: os.ErrNotExist, 191 | }, 192 | { 193 | name: "first not found", 194 | fns: []configFunc{ 195 | notExist, 196 | ok, 197 | }, 198 | }, 199 | { 200 | name: "first ok", 201 | fns: []configFunc{ 202 | ok, 203 | willPanic, 204 | }, 205 | }, 206 | } 207 | 208 | for _, tt := range tests { 209 | t.Run(tt.name, func(t *testing.T) { 210 | var cs []wginternal.Client 211 | for _, fn := range tt.fns { 212 | cs = append(cs, &testClient{ 213 | ConfigureDeviceFunc: fn, 214 | }) 215 | } 216 | 217 | c := &Client{cs: cs} 218 | 219 | err := c.ConfigureDevice("", wgtypes.Config{}) 220 | if diff := cmp.Diff(tt.err, err, cmpErrors); diff != "" { 221 | t.Fatalf("unexpected error (-want +got):\n%s", diff) 222 | } 223 | }) 224 | } 225 | } 226 | 227 | type testClient struct { 228 | CloseFunc func() error 229 | DevicesFunc func() ([]*wgtypes.Device, error) 230 | DeviceFunc func(name string) (*wgtypes.Device, error) 231 | ConfigureDeviceFunc func(name string, cfg wgtypes.Config) error 232 | } 233 | 234 | func (c *testClient) Close() error { return c.CloseFunc() } 235 | func (c *testClient) Devices() ([]*wgtypes.Device, error) { return c.DevicesFunc() } 236 | func (c *testClient) Device(name string) (*wgtypes.Device, error) { 237 | return c.DeviceFunc(name) 238 | } 239 | 240 | func (c *testClient) ConfigureDevice(name string, cfg wgtypes.Config) error { 241 | return c.ConfigureDeviceFunc(name, cfg) 242 | } 243 | -------------------------------------------------------------------------------- /internal/wguser/parse_test.go: -------------------------------------------------------------------------------- 1 | package wguser 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | "time" 7 | 8 | "github.com/google/go-cmp/cmp" 9 | "golang.zx2c4.com/wireguard/wgctrl/wgtypes" 10 | ) 11 | 12 | // Example string source (with some slight modifications to use all fields): 13 | // https://www.wireguard.com/xplatform/#example-dialog. 14 | const okGet = `private_key=e84b5a6d2717c1003a13b431570353dbaca9146cf150c5f8575680feba52027a 15 | listen_port=12912 16 | fwmark=1 17 | public_key=b85996fecc9c7f1fc6d2572a76eda11d59bcd20be8e543b15ce4bd85a8e75a33 18 | preshared_key=188515093e952f5f22e865cef3012e72f8b5f0b598ac0309d5dacce3b70fcf52 19 | allowed_ip=192.168.4.4/32 20 | endpoint=[abcd:23::33%2]:51820 21 | last_handshake_time_sec=1 22 | last_handshake_time_nsec=2 23 | public_key=58402e695ba1772b1cc9309755f043251ea77fdcf10fbe63989ceb7e19321376 24 | tx_bytes=38333 25 | rx_bytes=2224 26 | allowed_ip=192.168.4.6/32 27 | persistent_keepalive_interval=111 28 | endpoint=182.122.22.19:3233 29 | last_handshake_time_sec=0 30 | last_handshake_time_nsec=0 31 | public_key=662e14fd594556f522604703340351258903b64f35553763f19426ab2a515c58 32 | endpoint=5.152.198.39:51820 33 | last_handshake_time_sec=0 34 | last_handshake_time_nsec=0 35 | allowed_ip=192.168.4.10/32 36 | allowed_ip=192.168.4.11/32 37 | tx_bytes=1212111 38 | rx_bytes=1929999999 39 | protocol_version=1 40 | errno=0 41 | 42 | ` 43 | 44 | func TestClientDevices(t *testing.T) { 45 | // Used to trigger "parse peers" mode easily. 46 | const okKey = "public_key=0000000000000000000000000000000000000000000000000000000000000000\n" 47 | 48 | tests := []struct { 49 | name string 50 | res []byte 51 | ok bool 52 | d *wgtypes.Device 53 | }{ 54 | { 55 | name: "invalid key=value", 56 | res: []byte("foo=bar=baz"), 57 | }, 58 | { 59 | name: "invalid public_key", 60 | res: []byte("public_key=xxx"), 61 | }, 62 | { 63 | name: "short public_key", 64 | res: []byte("public_key=abcd"), 65 | }, 66 | { 67 | name: "invalid fwmark", 68 | res: []byte("fwmark=foo"), 69 | }, 70 | { 71 | name: "invalid endpoint", 72 | res: []byte(okKey + "endpoint=foo"), 73 | }, 74 | { 75 | name: "invalid allowed_ip", 76 | res: []byte(okKey + "allowed_ip=foo"), 77 | }, 78 | { 79 | name: "error", 80 | res: []byte("errno=2\n\n"), 81 | }, 82 | { 83 | name: "ok", 84 | res: []byte(okGet), 85 | ok: true, 86 | d: &wgtypes.Device{ 87 | Name: testDevice, 88 | Type: wgtypes.Userspace, 89 | PrivateKey: wgtypes.Key{0xe8, 0x4b, 0x5a, 0x6d, 0x27, 0x17, 0xc1, 0x0, 0x3a, 0x13, 0xb4, 0x31, 0x57, 0x3, 0x53, 0xdb, 0xac, 0xa9, 0x14, 0x6c, 0xf1, 0x50, 0xc5, 0xf8, 0x57, 0x56, 0x80, 0xfe, 0xba, 0x52, 0x2, 0x7a}, PublicKey: wgtypes.Key{0xc1, 0x53, 0x2e, 0x1b, 0x3d, 0x35, 0x8, 0xfc, 0x7e, 0xbc, 0x35, 0x4f, 0xa6, 0x79, 0x62, 0xf, 0x33, 0xf2, 0x87, 0x14, 0x95, 0x42, 0xe6, 0x84, 0xc6, 0x7b, 0x7b, 0xd, 0x81, 0x36, 0x2b, 0x29}, 90 | ListenPort: 12912, 91 | FirewallMark: 1, 92 | Peers: []wgtypes.Peer{ 93 | { 94 | PublicKey: wgtypes.Key{0xb8, 0x59, 0x96, 0xfe, 0xcc, 0x9c, 0x7f, 0x1f, 0xc6, 0xd2, 0x57, 0x2a, 0x76, 0xed, 0xa1, 0x1d, 0x59, 0xbc, 0xd2, 0xb, 0xe8, 0xe5, 0x43, 0xb1, 0x5c, 0xe4, 0xbd, 0x85, 0xa8, 0xe7, 0x5a, 0x33}, 95 | PresharedKey: wgtypes.Key{0x18, 0x85, 0x15, 0x9, 0x3e, 0x95, 0x2f, 0x5f, 0x22, 0xe8, 0x65, 0xce, 0xf3, 0x1, 0x2e, 0x72, 0xf8, 0xb5, 0xf0, 0xb5, 0x98, 0xac, 0x3, 0x9, 0xd5, 0xda, 0xcc, 0xe3, 0xb7, 0xf, 0xcf, 0x52}, 96 | Endpoint: &net.UDPAddr{ 97 | IP: net.ParseIP("abcd:23::33"), 98 | Port: 51820, 99 | Zone: "2", 100 | }, 101 | LastHandshakeTime: time.Unix(1, 2), 102 | AllowedIPs: []net.IPNet{ 103 | { 104 | IP: net.IP{0xc0, 0xa8, 0x4, 0x4}, 105 | Mask: net.IPMask{0xff, 0xff, 0xff, 0xff}, 106 | }, 107 | }, 108 | }, 109 | { 110 | PublicKey: wgtypes.Key{0x58, 0x40, 0x2e, 0x69, 0x5b, 0xa1, 0x77, 0x2b, 0x1c, 0xc9, 0x30, 0x97, 0x55, 0xf0, 0x43, 0x25, 0x1e, 0xa7, 0x7f, 0xdc, 0xf1, 0xf, 0xbe, 0x63, 0x98, 0x9c, 0xeb, 0x7e, 0x19, 0x32, 0x13, 0x76}, 111 | PresharedKey: wgtypes.Key{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, 112 | Endpoint: &net.UDPAddr{ 113 | IP: net.IPv4(182, 122, 22, 19), 114 | Port: 3233, 115 | }, 116 | // Zero-value because UNIX timestamp of 0. Explicitly 117 | // set for documentation purposes here. 118 | LastHandshakeTime: time.Time{}, 119 | PersistentKeepaliveInterval: 111000000000, 120 | ReceiveBytes: 2224, 121 | TransmitBytes: 38333, 122 | AllowedIPs: []net.IPNet{ 123 | { 124 | IP: net.IP{0xc0, 0xa8, 0x4, 0x6}, 125 | Mask: net.IPMask{0xff, 0xff, 0xff, 0xff}, 126 | }, 127 | }, 128 | }, 129 | { 130 | PublicKey: wgtypes.Key{0x66, 0x2e, 0x14, 0xfd, 0x59, 0x45, 0x56, 0xf5, 0x22, 0x60, 0x47, 0x3, 0x34, 0x3, 0x51, 0x25, 0x89, 0x3, 0xb6, 0x4f, 0x35, 0x55, 0x37, 0x63, 0xf1, 0x94, 0x26, 0xab, 0x2a, 0x51, 0x5c, 0x58}, 131 | Endpoint: &net.UDPAddr{ 132 | IP: net.IPv4(5, 152, 198, 39), 133 | Port: 51820, 134 | }, 135 | ReceiveBytes: 1929999999, 136 | TransmitBytes: 1212111, 137 | AllowedIPs: []net.IPNet{ 138 | { 139 | IP: net.IP{0xc0, 0xa8, 0x4, 0xa}, 140 | Mask: net.IPMask{0xff, 0xff, 0xff, 0xff}, 141 | }, 142 | { 143 | IP: net.IP{0xc0, 0xa8, 0x4, 0xb}, 144 | Mask: net.IPMask{0xff, 0xff, 0xff, 0xff}, 145 | }, 146 | }, 147 | ProtocolVersion: 1, 148 | }, 149 | }, 150 | }, 151 | }, 152 | } 153 | 154 | for _, tt := range tests { 155 | t.Run(tt.name, func(t *testing.T) { 156 | c, done := testClient(t, tt.res) 157 | defer done() 158 | 159 | devs, err := c.Devices() 160 | 161 | if tt.ok && err != nil { 162 | t.Fatalf("failed to get devices: %v", err) 163 | } 164 | if !tt.ok && err == nil { 165 | t.Fatal("expected an error, but none occurred") 166 | } 167 | if err != nil { 168 | return 169 | } 170 | 171 | if diff := cmp.Diff([]*wgtypes.Device{tt.d}, devs); diff != "" { 172 | t.Fatalf("unexpected Devices (-want +got):\n%s", diff) 173 | } 174 | }) 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /internal/wguser/parse.go: -------------------------------------------------------------------------------- 1 | package wguser 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/hex" 7 | "fmt" 8 | "io" 9 | "net" 10 | "os" 11 | "strconv" 12 | "time" 13 | 14 | "golang.zx2c4.com/wireguard/wgctrl/wgtypes" 15 | ) 16 | 17 | // The WireGuard userspace configuration protocol is described here: 18 | // https://www.wireguard.com/xplatform/#cross-platform-userspace-implementation. 19 | 20 | // getDevice gathers device information from a device specified by its path 21 | // and returns a Device. 22 | func (c *Client) getDevice(device string) (*wgtypes.Device, error) { 23 | conn, err := c.dial(device) 24 | if err != nil { 25 | return nil, err 26 | } 27 | defer conn.Close() 28 | 29 | // Get information about this device. 30 | if _, err := io.WriteString(conn, "get=1\n\n"); err != nil { 31 | return nil, err 32 | } 33 | 34 | // Parse the device from the incoming data stream. 35 | d, err := parseDevice(conn) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | // TODO(mdlayher): populate interface index too? 41 | d.Name = deviceName(device) 42 | d.Type = wgtypes.Userspace 43 | 44 | return d, nil 45 | } 46 | 47 | // parseDevice parses a Device and its Peers from an io.Reader. 48 | func parseDevice(r io.Reader) (*wgtypes.Device, error) { 49 | var dp deviceParser 50 | s := bufio.NewScanner(r) 51 | for s.Scan() { 52 | b := s.Bytes() 53 | if len(b) == 0 { 54 | // Empty line, done parsing. 55 | break 56 | } 57 | 58 | // All data is in key=value format. 59 | kvs := bytes.Split(b, []byte("=")) 60 | if len(kvs) != 2 { 61 | return nil, fmt.Errorf("wguser: invalid key=value pair: %q", string(b)) 62 | } 63 | 64 | dp.Parse(string(kvs[0]), string(kvs[1])) 65 | } 66 | 67 | if err := s.Err(); err != nil { 68 | return nil, err 69 | } 70 | 71 | return dp.Device() 72 | } 73 | 74 | // A deviceParser accumulates information about a Device and its Peers. 75 | type deviceParser struct { 76 | d wgtypes.Device 77 | err error 78 | 79 | parsePeers bool 80 | peers int 81 | hsSec, hsNano int 82 | } 83 | 84 | // Device returns a Device or any errors that were encountered while parsing 85 | // a Device. 86 | func (dp *deviceParser) Device() (*wgtypes.Device, error) { 87 | if dp.err != nil { 88 | return nil, dp.err 89 | } 90 | 91 | // Compute remaining fields of the Device now that all parsing is done. 92 | dp.d.PublicKey = dp.d.PrivateKey.PublicKey() 93 | 94 | return &dp.d, nil 95 | } 96 | 97 | // Parse parses a single key/value pair into fields of a Device. 98 | func (dp *deviceParser) Parse(key, value string) { 99 | switch key { 100 | case "errno": 101 | // 0 indicates success, anything else returns an error number that matches 102 | // definitions from errno.h. 103 | if errno := dp.parseInt(value); errno != 0 { 104 | // TODO(mdlayher): return actual errno on Linux? 105 | dp.err = os.NewSyscallError("read", fmt.Errorf("wguser: errno=%d", errno)) 106 | return 107 | } 108 | case "public_key": 109 | // We've either found the first peer or the next peer. Stop parsing 110 | // Device fields and start parsing Peer fields, including the public 111 | // key indicated here. 112 | dp.parsePeers = true 113 | dp.peers++ 114 | 115 | dp.d.Peers = append(dp.d.Peers, wgtypes.Peer{ 116 | PublicKey: dp.parseKey(value), 117 | }) 118 | return 119 | } 120 | 121 | // Are we parsing peer fields? 122 | if dp.parsePeers { 123 | dp.peerParse(key, value) 124 | return 125 | } 126 | 127 | // Device field parsing. 128 | switch key { 129 | case "private_key": 130 | dp.d.PrivateKey = dp.parseKey(value) 131 | case "listen_port": 132 | dp.d.ListenPort = dp.parseInt(value) 133 | case "fwmark": 134 | dp.d.FirewallMark = dp.parseInt(value) 135 | } 136 | } 137 | 138 | // curPeer returns the current Peer being parsed so its fields can be populated. 139 | func (dp *deviceParser) curPeer() *wgtypes.Peer { 140 | return &dp.d.Peers[dp.peers-1] 141 | } 142 | 143 | // peerParse parses a key/value field into the current Peer. 144 | func (dp *deviceParser) peerParse(key, value string) { 145 | p := dp.curPeer() 146 | switch key { 147 | case "preshared_key": 148 | p.PresharedKey = dp.parseKey(value) 149 | case "endpoint": 150 | p.Endpoint = dp.parseAddr(value) 151 | case "last_handshake_time_sec": 152 | dp.hsSec = dp.parseInt(value) 153 | case "last_handshake_time_nsec": 154 | dp.hsNano = dp.parseInt(value) 155 | 156 | // Assume that we've seen both seconds and nanoseconds and populate this 157 | // field now. However, if both fields were set to 0, assume we have never 158 | // had a successful handshake with this peer, and return a zero-value 159 | // time.Time to our callers. 160 | if dp.hsSec > 0 && dp.hsNano > 0 { 161 | p.LastHandshakeTime = time.Unix(int64(dp.hsSec), int64(dp.hsNano)) 162 | } 163 | case "tx_bytes": 164 | p.TransmitBytes = dp.parseInt64(value) 165 | case "rx_bytes": 166 | p.ReceiveBytes = dp.parseInt64(value) 167 | case "persistent_keepalive_interval": 168 | p.PersistentKeepaliveInterval = time.Duration(dp.parseInt(value)) * time.Second 169 | case "allowed_ip": 170 | cidr := dp.parseCIDR(value) 171 | if cidr != nil { 172 | p.AllowedIPs = append(p.AllowedIPs, *cidr) 173 | } 174 | case "protocol_version": 175 | p.ProtocolVersion = dp.parseInt(value) 176 | } 177 | } 178 | 179 | // parseKey parses a Key from a hex string. 180 | func (dp *deviceParser) parseKey(s string) wgtypes.Key { 181 | if dp.err != nil { 182 | return wgtypes.Key{} 183 | } 184 | 185 | b, err := hex.DecodeString(s) 186 | if err != nil { 187 | dp.err = err 188 | return wgtypes.Key{} 189 | } 190 | 191 | key, err := wgtypes.NewKey(b) 192 | if err != nil { 193 | dp.err = err 194 | return wgtypes.Key{} 195 | } 196 | 197 | return key 198 | } 199 | 200 | // parseInt parses an integer from a string. 201 | func (dp *deviceParser) parseInt(s string) int { 202 | if dp.err != nil { 203 | return 0 204 | } 205 | 206 | v, err := strconv.Atoi(s) 207 | if err != nil { 208 | dp.err = err 209 | return 0 210 | } 211 | 212 | return v 213 | } 214 | 215 | // parseInt64 parses an int64 from a string. 216 | func (dp *deviceParser) parseInt64(s string) int64 { 217 | if dp.err != nil { 218 | return 0 219 | } 220 | 221 | v, err := strconv.ParseInt(s, 10, 64) 222 | if err != nil { 223 | dp.err = err 224 | return 0 225 | } 226 | 227 | return v 228 | } 229 | 230 | // parseAddr parses a UDP address from a string. 231 | func (dp *deviceParser) parseAddr(s string) *net.UDPAddr { 232 | if dp.err != nil { 233 | return nil 234 | } 235 | 236 | addr, err := net.ResolveUDPAddr("udp", s) 237 | if err != nil { 238 | dp.err = err 239 | return nil 240 | } 241 | 242 | return addr 243 | } 244 | 245 | // parseInt parses an address CIDR from a string. 246 | func (dp *deviceParser) parseCIDR(s string) *net.IPNet { 247 | if dp.err != nil { 248 | return nil 249 | } 250 | 251 | _, cidr, err := net.ParseCIDR(s) 252 | if err != nil { 253 | dp.err = err 254 | return nil 255 | } 256 | 257 | return cidr 258 | } 259 | -------------------------------------------------------------------------------- /internal/wglinux/client_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package wglinux 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "os" 10 | "syscall" 11 | 12 | "github.com/mdlayher/genetlink" 13 | "github.com/mdlayher/netlink" 14 | "github.com/mdlayher/netlink/nlenc" 15 | "golang.org/x/sys/unix" 16 | "golang.zx2c4.com/wireguard/wgctrl/internal/wginternal" 17 | "golang.zx2c4.com/wireguard/wgctrl/wgtypes" 18 | ) 19 | 20 | var _ wginternal.Client = &Client{} 21 | 22 | // A Client provides access to Linux WireGuard netlink information. 23 | type Client struct { 24 | c *genetlink.Conn 25 | family genetlink.Family 26 | 27 | interfaces func() ([]string, error) 28 | } 29 | 30 | // New creates a new Client and returns whether or not the generic netlink 31 | // interface is available. 32 | func New() (*Client, bool, error) { 33 | c, err := genetlink.Dial(nil) 34 | if err != nil { 35 | return nil, false, err 36 | } 37 | 38 | // Best effort version of netlink.Config.Strict due to CentOS 7. 39 | for _, o := range []netlink.ConnOption{ 40 | netlink.ExtendedAcknowledge, 41 | netlink.GetStrictCheck, 42 | } { 43 | _ = c.SetOption(o, true) 44 | } 45 | 46 | return initClient(c) 47 | } 48 | 49 | // initClient is the internal Client constructor used in some tests. 50 | func initClient(c *genetlink.Conn) (*Client, bool, error) { 51 | f, err := c.GetFamily(unix.WG_GENL_NAME) 52 | if err != nil { 53 | _ = c.Close() 54 | 55 | if errors.Is(err, os.ErrNotExist) { 56 | // The generic netlink interface is not available. 57 | return nil, false, nil 58 | } 59 | 60 | return nil, false, err 61 | } 62 | 63 | return &Client{ 64 | c: c, 65 | family: f, 66 | 67 | // By default, gather only WireGuard interfaces using rtnetlink. 68 | interfaces: rtnlInterfaces, 69 | }, true, nil 70 | } 71 | 72 | // Close implements wginternal.Client. 73 | func (c *Client) Close() error { 74 | return c.c.Close() 75 | } 76 | 77 | // Devices implements wginternal.Client. 78 | func (c *Client) Devices() ([]*wgtypes.Device, error) { 79 | // By default, rtnetlink is used to fetch a list of all interfaces and then 80 | // filter that list to only find WireGuard interfaces. 81 | // 82 | // The remainder of this function assumes that any returned device from this 83 | // function is a valid WireGuard device. 84 | ifis, err := c.interfaces() 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | ds := make([]*wgtypes.Device, 0, len(ifis)) 90 | for _, ifi := range ifis { 91 | d, err := c.Device(ifi) 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | ds = append(ds, d) 97 | } 98 | 99 | return ds, nil 100 | } 101 | 102 | // Device implements wginternal.Client. 103 | func (c *Client) Device(name string) (*wgtypes.Device, error) { 104 | // Don't bother querying netlink with empty input. 105 | if name == "" { 106 | return nil, os.ErrNotExist 107 | } 108 | 109 | // Fetching a device by interface index is possible as well, but we only 110 | // support fetching by name as it seems to be more convenient in general. 111 | b, err := netlink.MarshalAttributes([]netlink.Attribute{{ 112 | Type: unix.WGDEVICE_A_IFNAME, 113 | Data: nlenc.Bytes(name), 114 | }}) 115 | if err != nil { 116 | return nil, err 117 | } 118 | 119 | msgs, err := c.execute(unix.WG_CMD_GET_DEVICE, netlink.Request|netlink.Dump, b) 120 | if err != nil { 121 | return nil, err 122 | } 123 | 124 | return parseDevice(msgs) 125 | } 126 | 127 | // ConfigureDevice implements wginternal.Client. 128 | func (c *Client) ConfigureDevice(name string, cfg wgtypes.Config) error { 129 | // Large configurations are split into batches for use with netlink. 130 | for _, b := range buildBatches(cfg) { 131 | attrs, err := configAttrs(name, b) 132 | if err != nil { 133 | return err 134 | } 135 | 136 | // Request acknowledgement of our request from netlink, even though the 137 | // output messages are unused. The netlink package checks and trims the 138 | // status code value. 139 | if _, err := c.execute(unix.WG_CMD_SET_DEVICE, netlink.Request|netlink.Acknowledge, attrs); err != nil { 140 | return err 141 | } 142 | } 143 | 144 | return nil 145 | } 146 | 147 | // execute executes a single WireGuard netlink request with the specified command, 148 | // header flags, and attribute arguments. 149 | func (c *Client) execute(command uint8, flags netlink.HeaderFlags, attrb []byte) ([]genetlink.Message, error) { 150 | msg := genetlink.Message{ 151 | Header: genetlink.Header{ 152 | Command: command, 153 | Version: unix.WG_GENL_VERSION, 154 | }, 155 | Data: attrb, 156 | } 157 | 158 | msgs, err := c.c.Execute(msg, c.family.ID, flags) 159 | if err == nil { 160 | return msgs, nil 161 | } 162 | 163 | // We don't want to expose netlink errors directly to callers so unpack to 164 | // something more generic. 165 | oerr, ok := err.(*netlink.OpError) 166 | if !ok { 167 | // Expect all errors to conform to netlink.OpError. 168 | return nil, fmt.Errorf("wglinux: netlink operation returned non-netlink error (please file a bug: https://golang.zx2c4.com/wireguard/wgctrl): %v", err) 169 | } 170 | 171 | switch oerr.Err { 172 | // Convert "no such device" and "not a wireguard device" to an error 173 | // compatible with os.ErrNotExist for easy checking. 174 | case unix.ENODEV, unix.ENOTSUP: 175 | return nil, os.ErrNotExist 176 | default: 177 | // Expose the inner error directly (such as EPERM). 178 | return nil, oerr.Err 179 | } 180 | } 181 | 182 | // rtnlInterfaces uses rtnetlink to fetch a list of WireGuard interfaces. 183 | func rtnlInterfaces() ([]string, error) { 184 | // Use the stdlib's rtnetlink helpers to get ahold of a table of all 185 | // interfaces, so we can begin filtering it down to just WireGuard devices. 186 | tab, err := syscall.NetlinkRIB(unix.RTM_GETLINK, unix.AF_UNSPEC) 187 | if err != nil { 188 | return nil, fmt.Errorf("wglinux: failed to get list of interfaces from rtnetlink: %v", err) 189 | } 190 | 191 | msgs, err := syscall.ParseNetlinkMessage(tab) 192 | if err != nil { 193 | return nil, fmt.Errorf("wglinux: failed to parse rtnetlink messages: %v", err) 194 | } 195 | 196 | return parseRTNLInterfaces(msgs) 197 | } 198 | 199 | // parseRTNLInterfaces unpacks rtnetlink messages and returns WireGuard 200 | // interface names. 201 | func parseRTNLInterfaces(msgs []syscall.NetlinkMessage) ([]string, error) { 202 | var ifis []string 203 | for _, m := range msgs { 204 | // Only deal with link messages, and they must have an ifinfomsg 205 | // structure appear before the attributes. 206 | if m.Header.Type != unix.RTM_NEWLINK { 207 | continue 208 | } 209 | 210 | if len(m.Data) < unix.SizeofIfInfomsg { 211 | return nil, fmt.Errorf("wglinux: rtnetlink message is too short for ifinfomsg: %d", len(m.Data)) 212 | } 213 | 214 | ad, err := netlink.NewAttributeDecoder(m.Data[syscall.SizeofIfInfomsg:]) 215 | if err != nil { 216 | return nil, err 217 | } 218 | 219 | // Determine the interface's name and if it's a WireGuard device. 220 | var ( 221 | ifi string 222 | isWG bool 223 | ) 224 | 225 | for ad.Next() { 226 | switch ad.Type() { 227 | case unix.IFLA_IFNAME: 228 | ifi = ad.String() 229 | case unix.IFLA_LINKINFO: 230 | ad.Do(isWGKind(&isWG)) 231 | } 232 | } 233 | 234 | if err := ad.Err(); err != nil { 235 | return nil, err 236 | } 237 | 238 | if isWG { 239 | // Found one; append it to the list. 240 | ifis = append(ifis, ifi) 241 | } 242 | } 243 | 244 | return ifis, nil 245 | } 246 | 247 | // wgKind is the IFLA_INFO_KIND value for WireGuard devices. 248 | const wgKind = "wireguard" 249 | 250 | // isWGKind parses netlink attributes to determine if a link is a WireGuard 251 | // device, then populates ok with the result. 252 | func isWGKind(ok *bool) func(b []byte) error { 253 | return func(b []byte) error { 254 | ad, err := netlink.NewAttributeDecoder(b) 255 | if err != nil { 256 | return err 257 | } 258 | 259 | for ad.Next() { 260 | if ad.Type() != unix.IFLA_INFO_KIND { 261 | continue 262 | } 263 | 264 | if ad.String() == wgKind { 265 | *ok = true 266 | return nil 267 | } 268 | } 269 | 270 | return ad.Err() 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /wgtypes/types.go: -------------------------------------------------------------------------------- 1 | package wgtypes 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base64" 6 | "fmt" 7 | "net" 8 | "time" 9 | 10 | "golang.org/x/crypto/curve25519" 11 | ) 12 | 13 | // A DeviceType specifies the underlying implementation of a WireGuard device. 14 | type DeviceType int 15 | 16 | // Possible DeviceType values. 17 | const ( 18 | Unknown DeviceType = iota 19 | LinuxKernel 20 | OpenBSDKernel 21 | FreeBSDKernel 22 | WindowsKernel 23 | Userspace 24 | ) 25 | 26 | // String returns the string representation of a DeviceType. 27 | func (dt DeviceType) String() string { 28 | switch dt { 29 | case LinuxKernel: 30 | return "Linux kernel" 31 | case OpenBSDKernel: 32 | return "OpenBSD kernel" 33 | case FreeBSDKernel: 34 | return "FreeBSD kernel" 35 | case WindowsKernel: 36 | return "Windows kernel" 37 | case Userspace: 38 | return "userspace" 39 | default: 40 | return "unknown" 41 | } 42 | } 43 | 44 | // A Device is a WireGuard device. 45 | type Device struct { 46 | // Name is the name of the device. 47 | Name string 48 | 49 | // Type specifies the underlying implementation of the device. 50 | Type DeviceType 51 | 52 | // PrivateKey is the device's private key. 53 | PrivateKey Key 54 | 55 | // PublicKey is the device's public key, computed from its PrivateKey. 56 | PublicKey Key 57 | 58 | // ListenPort is the device's network listening port. 59 | ListenPort int 60 | 61 | // FirewallMark is the device's current firewall mark. 62 | // 63 | // The firewall mark can be used in conjunction with firewall software to 64 | // take action on outgoing WireGuard packets. 65 | FirewallMark int 66 | 67 | // Peers is the list of network peers associated with this device. 68 | Peers []Peer 69 | } 70 | 71 | // KeyLen is the expected key length for a WireGuard key. 72 | const KeyLen = 32 // wgh.KeyLen 73 | 74 | // A Key is a public, private, or pre-shared secret key. The Key constructor 75 | // functions in this package can be used to create Keys suitable for each of 76 | // these applications. 77 | type Key [KeyLen]byte 78 | 79 | // GenerateKey generates a Key suitable for use as a pre-shared secret key from 80 | // a cryptographically safe source. 81 | // 82 | // The output Key should not be used as a private key; use GeneratePrivateKey 83 | // instead. 84 | func GenerateKey() (Key, error) { 85 | b := make([]byte, KeyLen) 86 | if _, err := rand.Read(b); err != nil { 87 | return Key{}, fmt.Errorf("wgtypes: failed to read random bytes: %v", err) 88 | } 89 | 90 | return NewKey(b) 91 | } 92 | 93 | // GeneratePrivateKey generates a Key suitable for use as a private key from a 94 | // cryptographically safe source. 95 | func GeneratePrivateKey() (Key, error) { 96 | key, err := GenerateKey() 97 | if err != nil { 98 | return Key{}, err 99 | } 100 | 101 | // Modify random bytes using algorithm described at: 102 | // https://cr.yp.to/ecdh.html. 103 | key[0] &= 248 104 | key[31] &= 127 105 | key[31] |= 64 106 | 107 | return key, nil 108 | } 109 | 110 | // NewKey creates a Key from an existing byte slice. The byte slice must be 111 | // exactly 32 bytes in length. 112 | func NewKey(b []byte) (Key, error) { 113 | if len(b) != KeyLen { 114 | return Key{}, fmt.Errorf("wgtypes: incorrect key size: %d", len(b)) 115 | } 116 | 117 | var k Key 118 | copy(k[:], b) 119 | 120 | return k, nil 121 | } 122 | 123 | // ParseKey parses a Key from a base64-encoded string, as produced by the 124 | // Key.String method. 125 | func ParseKey(s string) (Key, error) { 126 | b, err := base64.StdEncoding.DecodeString(s) 127 | if err != nil { 128 | return Key{}, fmt.Errorf("wgtypes: failed to parse base64-encoded key: %v", err) 129 | } 130 | 131 | return NewKey(b) 132 | } 133 | 134 | // PublicKey computes a public key from the private key k. 135 | // 136 | // PublicKey should only be called when k is a private key. 137 | func (k Key) PublicKey() Key { 138 | var ( 139 | pub [KeyLen]byte 140 | priv = [KeyLen]byte(k) 141 | ) 142 | 143 | // ScalarBaseMult uses the correct base value per https://cr.yp.to/ecdh.html, 144 | // so no need to specify it. 145 | curve25519.ScalarBaseMult(&pub, &priv) 146 | 147 | return Key(pub) 148 | } 149 | 150 | // String returns the base64-encoded string representation of a Key. 151 | // 152 | // ParseKey can be used to produce a new Key from this string. 153 | func (k Key) String() string { 154 | return base64.StdEncoding.EncodeToString(k[:]) 155 | } 156 | 157 | // A Peer is a WireGuard peer to a Device. 158 | type Peer struct { 159 | // PublicKey is the public key of a peer, computed from its private key. 160 | // 161 | // PublicKey is always present in a Peer. 162 | PublicKey Key 163 | 164 | // PresharedKey is an optional preshared key which may be used as an 165 | // additional layer of security for peer communications. 166 | // 167 | // A zero-value Key means no preshared key is configured. 168 | PresharedKey Key 169 | 170 | // Endpoint is the most recent source address used for communication by 171 | // this Peer. 172 | Endpoint *net.UDPAddr 173 | 174 | // PersistentKeepaliveInterval specifies how often an "empty" packet is sent 175 | // to a peer to keep a connection alive. 176 | // 177 | // A value of 0 indicates that persistent keepalives are disabled. 178 | PersistentKeepaliveInterval time.Duration 179 | 180 | // LastHandshakeTime indicates the most recent time a handshake was performed 181 | // with this peer. 182 | // 183 | // A zero-value time.Time indicates that no handshake has taken place with 184 | // this peer. 185 | LastHandshakeTime time.Time 186 | 187 | // ReceiveBytes indicates the number of bytes received from this peer. 188 | ReceiveBytes int64 189 | 190 | // TransmitBytes indicates the number of bytes transmitted to this peer. 191 | TransmitBytes int64 192 | 193 | // AllowedIPs specifies which IPv4 and IPv6 addresses this peer is allowed 194 | // to communicate on. 195 | // 196 | // 0.0.0.0/0 indicates that all IPv4 addresses are allowed, and ::/0 197 | // indicates that all IPv6 addresses are allowed. 198 | AllowedIPs []net.IPNet 199 | 200 | // ProtocolVersion specifies which version of the WireGuard protocol is used 201 | // for this Peer. 202 | // 203 | // A value of 0 indicates that the most recent protocol version will be used. 204 | ProtocolVersion int 205 | } 206 | 207 | // A Config is a WireGuard device configuration. 208 | // 209 | // Because the zero value of some Go types may be significant to WireGuard for 210 | // Config fields, pointer types are used for some of these fields. Only 211 | // pointer fields which are not nil will be applied when configuring a device. 212 | type Config struct { 213 | // PrivateKey specifies a private key configuration, if not nil. 214 | // 215 | // A non-nil, zero-value Key will clear the private key. 216 | PrivateKey *Key 217 | 218 | // ListenPort specifies a device's listening port, if not nil. 219 | ListenPort *int 220 | 221 | // FirewallMark specifies a device's firewall mark, if not nil. 222 | // 223 | // If non-nil and set to 0, the firewall mark will be cleared. 224 | FirewallMark *int 225 | 226 | // ReplacePeers specifies if the Peers in this configuration should replace 227 | // the existing peer list, instead of appending them to the existing list. 228 | ReplacePeers bool 229 | 230 | // Peers specifies a list of peer configurations to apply to a device. 231 | Peers []PeerConfig 232 | } 233 | 234 | // TODO(mdlayher): consider adding ProtocolVersion in PeerConfig. 235 | 236 | // A PeerConfig is a WireGuard device peer configuration. 237 | // 238 | // Because the zero value of some Go types may be significant to WireGuard for 239 | // PeerConfig fields, pointer types are used for some of these fields. Only 240 | // pointer fields which are not nil will be applied when configuring a peer. 241 | type PeerConfig struct { 242 | // PublicKey specifies the public key of this peer. PublicKey is a 243 | // mandatory field for all PeerConfigs. 244 | PublicKey Key 245 | 246 | // Remove specifies if the peer with this public key should be removed 247 | // from a device's peer list. 248 | Remove bool 249 | 250 | // UpdateOnly specifies that an operation will only occur on this peer 251 | // if the peer already exists as part of the interface. 252 | UpdateOnly bool 253 | 254 | // PresharedKey specifies a peer's preshared key configuration, if not nil. 255 | // 256 | // A non-nil, zero-value Key will clear the preshared key. 257 | PresharedKey *Key 258 | 259 | // Endpoint specifies the endpoint of this peer entry, if not nil. 260 | Endpoint *net.UDPAddr 261 | 262 | // PersistentKeepaliveInterval specifies the persistent keepalive interval 263 | // for this peer, if not nil. 264 | // 265 | // A non-nil value of 0 will clear the persistent keepalive interval. 266 | PersistentKeepaliveInterval *time.Duration 267 | 268 | // ReplaceAllowedIPs specifies if the allowed IPs specified in this peer 269 | // configuration should replace any existing ones, instead of appending them 270 | // to the allowed IPs list. 271 | ReplaceAllowedIPs bool 272 | 273 | // AllowedIPs specifies a list of allowed IP addresses in CIDR notation 274 | // for this peer. 275 | AllowedIPs []net.IPNet 276 | } 277 | -------------------------------------------------------------------------------- /internal/wglinux/client_linux_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package wglinux 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "net" 10 | "os" 11 | "os/user" 12 | "syscall" 13 | "testing" 14 | "time" 15 | 16 | "github.com/google/go-cmp/cmp" 17 | "github.com/mdlayher/genetlink" 18 | "github.com/mdlayher/genetlink/genltest" 19 | "github.com/mdlayher/netlink" 20 | "github.com/mdlayher/netlink/nlenc" 21 | "github.com/mdlayher/netlink/nltest" 22 | "golang.org/x/sys/unix" 23 | "golang.zx2c4.com/wireguard/wgctrl/wgtypes" 24 | ) 25 | 26 | const ( 27 | okIndex = 1 28 | okName = "wg0" 29 | ) 30 | 31 | func TestLinuxClientDevicesEmpty(t *testing.T) { 32 | tests := []struct { 33 | name string 34 | fn func() ([]string, error) 35 | }{ 36 | { 37 | name: "no interfaces", 38 | fn: func() ([]string, error) { 39 | return nil, nil 40 | }, 41 | }, 42 | } 43 | 44 | for _, tt := range tests { 45 | t.Run(tt.name, func(t *testing.T) { 46 | c := testClient(t, func(_ genetlink.Message, _ netlink.Message) ([]genetlink.Message, error) { 47 | panic("no devices; shouldn't call genetlink") 48 | }) 49 | defer c.Close() 50 | 51 | c.interfaces = tt.fn 52 | 53 | ds, err := c.Devices() 54 | if err != nil { 55 | t.Fatalf("failed to get devices: %v", err) 56 | } 57 | 58 | if diff := cmp.Diff(0, len(ds)); diff != "" { 59 | t.Fatalf("unexpected number of devices (-want +got):\n%s", diff) 60 | } 61 | }) 62 | } 63 | } 64 | 65 | func TestLinuxClientIsNotExist(t *testing.T) { 66 | // TODO(mdlayher): not ideal but this test is not particularly load-bearing 67 | // and the entire *nltest ecosystem needs to be reworked. 68 | t.Skipf("skipping, genltest needs to be reworked") 69 | 70 | device := func(c *Client) error { 71 | _, err := c.Device("wg0") 72 | return err 73 | } 74 | 75 | configure := func(c *Client) error { 76 | return c.ConfigureDevice("wg0", wgtypes.Config{}) 77 | } 78 | 79 | tests := []struct { 80 | name string 81 | fn func(c *Client) error 82 | msgs []genetlink.Message 83 | errno unix.Errno 84 | }{ 85 | { 86 | name: "name: empty", 87 | fn: func(c *Client) error { 88 | _, err := c.Device("") 89 | return err 90 | }, 91 | }, 92 | { 93 | name: "name: ENODEV", 94 | fn: device, 95 | errno: unix.ENODEV, 96 | }, 97 | { 98 | name: "name: ENOTSUP", 99 | fn: device, 100 | errno: unix.ENOTSUP, 101 | }, 102 | { 103 | name: "configure: ENODEV", 104 | fn: configure, 105 | errno: unix.ENODEV, 106 | }, 107 | { 108 | name: "configure: ENOTSUP", 109 | fn: configure, 110 | errno: unix.ENOTSUP, 111 | }, 112 | } 113 | 114 | for _, tt := range tests { 115 | t.Run(tt.name, func(t *testing.T) { 116 | c := testClient(t, func(_ genetlink.Message, _ netlink.Message) ([]genetlink.Message, error) { 117 | // We aren't creating a system call error; we are creating a 118 | // netlink error inside a message. 119 | return tt.msgs, genltest.Error(int(tt.errno)) 120 | }) 121 | defer c.Close() 122 | 123 | if err := tt.fn(c); !errors.Is(err, os.ErrNotExist) { 124 | t.Fatalf("expected is not exist, but got: %v", err) 125 | } 126 | }) 127 | } 128 | } 129 | 130 | func TestLinuxClientIsPermission(t *testing.T) { 131 | u, err := user.Current() 132 | if err != nil { 133 | t.Fatalf("failed to get current user: %v", err) 134 | } 135 | if u.Uid == "0" { 136 | t.Skip("skipping, test must be run without elevated privileges") 137 | } 138 | 139 | c, ok, err := New() 140 | if err != nil { 141 | t.Fatalf("failed to create Client: %v", err) 142 | } 143 | if !ok { 144 | t.Skip("skipping, the WireGuard generic netlink API is not available") 145 | } 146 | 147 | defer c.Close() 148 | 149 | // Check for permission denied as unprivileged user. 150 | if _, err := c.Device("wgnotexist0"); !os.IsPermission(err) { 151 | t.Fatalf("expected permission denied, but got: %v", err) 152 | } 153 | } 154 | 155 | func Test_initClientNotExist(t *testing.T) { 156 | conn := genltest.Dial(func(_ genetlink.Message, _ netlink.Message) ([]genetlink.Message, error) { 157 | // Simulate genetlink family not found. 158 | return nil, genltest.Error(int(unix.ENOENT)) 159 | }) 160 | 161 | _, ok, err := initClient(conn) 162 | if err != nil { 163 | t.Fatalf("failed to open Client: %v", err) 164 | } 165 | if ok { 166 | t.Fatal("the generic netlink API should not be available from genltest") 167 | } 168 | } 169 | 170 | func Test_parseRTNLInterfaces(t *testing.T) { 171 | // marshalAttrs creates packed netlink attributes with a prepended ifinfomsg 172 | // structure, as returned by rtnetlink. 173 | marshalAttrs := func(attrs []netlink.Attribute) []byte { 174 | ifinfomsg := make([]byte, syscall.SizeofIfInfomsg) 175 | 176 | return append(ifinfomsg, nltest.MustMarshalAttributes(attrs)...) 177 | } 178 | 179 | tests := []struct { 180 | name string 181 | msgs []syscall.NetlinkMessage 182 | ifis []string 183 | ok bool 184 | }{ 185 | { 186 | name: "short ifinfomsg", 187 | msgs: []syscall.NetlinkMessage{{ 188 | Header: syscall.NlMsghdr{ 189 | Type: unix.RTM_NEWLINK, 190 | }, 191 | Data: []byte{0xff}, 192 | }}, 193 | }, 194 | { 195 | name: "empty", 196 | ok: true, 197 | }, 198 | { 199 | name: "immediate done", 200 | msgs: []syscall.NetlinkMessage{{ 201 | Header: syscall.NlMsghdr{ 202 | Type: unix.NLMSG_DONE, 203 | }, 204 | }}, 205 | ok: true, 206 | }, 207 | { 208 | name: "ok", 209 | msgs: []syscall.NetlinkMessage{ 210 | // Bridge device. 211 | { 212 | Header: syscall.NlMsghdr{ 213 | Type: unix.RTM_NEWLINK, 214 | }, 215 | Data: marshalAttrs([]netlink.Attribute{ 216 | { 217 | Type: unix.IFLA_IFNAME, 218 | Data: nlenc.Bytes("br0"), 219 | }, 220 | { 221 | Type: unix.IFLA_LINKINFO, 222 | Data: m(netlink.Attribute{ 223 | Type: unix.IFLA_INFO_KIND, 224 | Data: nlenc.Bytes("bridge"), 225 | }), 226 | }, 227 | }), 228 | }, 229 | // WireGuard device. 230 | { 231 | Header: syscall.NlMsghdr{ 232 | Type: unix.RTM_NEWLINK, 233 | }, 234 | Data: marshalAttrs([]netlink.Attribute{ 235 | { 236 | Type: unix.IFLA_IFNAME, 237 | Data: nlenc.Bytes(okName), 238 | }, 239 | { 240 | Type: unix.IFLA_LINKINFO, 241 | Data: m([]netlink.Attribute{ 242 | // Random junk to skip. 243 | { 244 | Type: 255, 245 | Data: nlenc.Uint16Bytes(0xff), 246 | }, 247 | { 248 | Type: unix.IFLA_INFO_KIND, 249 | Data: nlenc.Bytes(wgKind), 250 | }, 251 | }...), 252 | }, 253 | }), 254 | }, 255 | }, 256 | ifis: []string{okName}, 257 | ok: true, 258 | }, 259 | } 260 | 261 | for _, tt := range tests { 262 | t.Run(tt.name, func(t *testing.T) { 263 | ifis, err := parseRTNLInterfaces(tt.msgs) 264 | 265 | if tt.ok && err != nil { 266 | t.Fatalf("failed to parse interfaces: %v", err) 267 | } 268 | if !tt.ok && err == nil { 269 | t.Fatal("expected an error, but none occurred") 270 | } 271 | if err != nil { 272 | return 273 | } 274 | 275 | if diff := cmp.Diff(tt.ifis, ifis); diff != "" { 276 | t.Fatalf("unexpected interfaces (-want +got):\n%s", diff) 277 | } 278 | }) 279 | } 280 | } 281 | 282 | const familyID = 20 283 | 284 | func testClient(t *testing.T, fn genltest.Func) *Client { 285 | family := genetlink.Family{ 286 | ID: familyID, 287 | Version: unix.WG_GENL_VERSION, 288 | Name: unix.WG_GENL_NAME, 289 | } 290 | 291 | conn := genltest.Dial(genltest.ServeFamily(family, fn)) 292 | 293 | c, ok, err := initClient(conn) 294 | if err != nil { 295 | t.Fatalf("failed to open Client: %v", err) 296 | } 297 | if !ok { 298 | t.Fatal("the generic netlink API was not available from genltest") 299 | } 300 | 301 | c.interfaces = func() ([]string, error) { 302 | return []string{okName}, nil 303 | } 304 | 305 | return c 306 | } 307 | 308 | func diffAttrs(x, y []netlink.Attribute) string { 309 | // Make copies to avoid a race and then zero out length values 310 | // for comparison. 311 | xPrime := make([]netlink.Attribute, len(x)) 312 | copy(xPrime, x) 313 | 314 | for i := 0; i < len(xPrime); i++ { 315 | xPrime[i].Length = 0 316 | } 317 | 318 | yPrime := make([]netlink.Attribute, len(y)) 319 | copy(yPrime, y) 320 | 321 | for i := 0; i < len(yPrime); i++ { 322 | yPrime[i].Length = 0 323 | } 324 | 325 | return cmp.Diff(xPrime, yPrime) 326 | } 327 | 328 | func mustAllowedIPs(ipns []net.IPNet) []byte { 329 | ae := netlink.NewAttributeEncoder() 330 | if err := encodeAllowedIPs(ipns)(ae); err != nil { 331 | panicf("failed to create allowed IP attributes: %v", err) 332 | } 333 | 334 | b, err := ae.Encode() 335 | if err != nil { 336 | panicf("failed to encode allowed IP attributes: %v", err) 337 | } 338 | 339 | return b 340 | } 341 | 342 | func m(attrs ...netlink.Attribute) []byte { return nltest.MustMarshalAttributes(attrs) } 343 | 344 | func durPtr(d time.Duration) *time.Duration { return &d } 345 | func keyPtr(k wgtypes.Key) *wgtypes.Key { return &k } 346 | func intPtr(v int) *int { return &v } 347 | 348 | func panicf(format string, a ...interface{}) { 349 | panic(fmt.Sprintf(format, a...)) 350 | } 351 | -------------------------------------------------------------------------------- /internal/wglinux/configure_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package wglinux 5 | 6 | import ( 7 | "encoding/binary" 8 | "fmt" 9 | "net" 10 | "unsafe" 11 | 12 | "github.com/mdlayher/netlink" 13 | "github.com/mdlayher/netlink/nlenc" 14 | "golang.org/x/sys/unix" 15 | "golang.zx2c4.com/wireguard/wgctrl/wgtypes" 16 | ) 17 | 18 | // configAttrs creates the required encoded netlink attributes to configure 19 | // the device specified by name using the non-nil fields in cfg. 20 | func configAttrs(name string, cfg wgtypes.Config) ([]byte, error) { 21 | ae := netlink.NewAttributeEncoder() 22 | ae.String(unix.WGDEVICE_A_IFNAME, name) 23 | 24 | if cfg.PrivateKey != nil { 25 | ae.Bytes(unix.WGDEVICE_A_PRIVATE_KEY, (*cfg.PrivateKey)[:]) 26 | } 27 | 28 | if cfg.ListenPort != nil { 29 | ae.Uint16(unix.WGDEVICE_A_LISTEN_PORT, uint16(*cfg.ListenPort)) 30 | } 31 | 32 | if cfg.FirewallMark != nil { 33 | ae.Uint32(unix.WGDEVICE_A_FWMARK, uint32(*cfg.FirewallMark)) 34 | } 35 | 36 | if cfg.ReplacePeers { 37 | ae.Uint32(unix.WGDEVICE_A_FLAGS, unix.WGDEVICE_F_REPLACE_PEERS) 38 | } 39 | 40 | // Only apply peer attributes if necessary. 41 | if len(cfg.Peers) > 0 { 42 | ae.Nested(unix.WGDEVICE_A_PEERS, func(nae *netlink.AttributeEncoder) error { 43 | // Netlink arrays use type as an array index. 44 | for i, p := range cfg.Peers { 45 | nae.Nested(uint16(i), encodePeer(p)) 46 | } 47 | 48 | return nil 49 | }) 50 | } 51 | 52 | return ae.Encode() 53 | } 54 | 55 | // ipBatchChunk is a tunable allowed IP batch limit per peer. 56 | // 57 | // Because we don't necessarily know how much space a given peer will occupy, 58 | // we play it safe and use a reasonably small value. Note that this constant 59 | // is used both in this package and tests, so be aware when making changes. 60 | const ipBatchChunk = 256 61 | 62 | // peerBatchChunk specifies the number of peers that can appear in a 63 | // configuration before we start splitting it into chunks. 64 | const peerBatchChunk = 32 65 | 66 | // shouldBatch determines if a configuration is sufficiently complex that it 67 | // should be split into batches. 68 | func shouldBatch(cfg wgtypes.Config) bool { 69 | if len(cfg.Peers) > peerBatchChunk { 70 | return true 71 | } 72 | 73 | var ips int 74 | for _, p := range cfg.Peers { 75 | ips += len(p.AllowedIPs) 76 | } 77 | 78 | return ips > ipBatchChunk 79 | } 80 | 81 | // buildBatches produces a batch of configs from a single config, if needed. 82 | func buildBatches(cfg wgtypes.Config) []wgtypes.Config { 83 | // Is this a small configuration; no need to batch? 84 | if !shouldBatch(cfg) { 85 | return []wgtypes.Config{cfg} 86 | } 87 | 88 | // Use most fields of cfg for our "base" configuration, and only differ 89 | // peers in each batch. 90 | base := cfg 91 | base.Peers = nil 92 | 93 | // Track the known peers so that peer IPs are not replaced if a single 94 | // peer has its allowed IPs split into multiple batches. 95 | knownPeers := make(map[wgtypes.Key]struct{}) 96 | 97 | batches := make([]wgtypes.Config, 0) 98 | for _, p := range cfg.Peers { 99 | batch := base 100 | 101 | // Iterate until no more allowed IPs. 102 | var done bool 103 | for !done { 104 | var tmp []net.IPNet 105 | if len(p.AllowedIPs) < ipBatchChunk { 106 | // IPs all fit within a batch; we are done. 107 | tmp = make([]net.IPNet, len(p.AllowedIPs)) 108 | copy(tmp, p.AllowedIPs) 109 | done = true 110 | } else { 111 | // IPs are larger than a single batch, copy a batch out and 112 | // advance the cursor. 113 | tmp = make([]net.IPNet, ipBatchChunk) 114 | copy(tmp, p.AllowedIPs[:ipBatchChunk]) 115 | 116 | p.AllowedIPs = p.AllowedIPs[ipBatchChunk:] 117 | 118 | if len(p.AllowedIPs) == 0 { 119 | // IPs ended on a batch boundary; no more IPs left so end 120 | // iteration after this loop. 121 | done = true 122 | } 123 | } 124 | 125 | pcfg := wgtypes.PeerConfig{ 126 | // PublicKey denotes the peer and must be present. 127 | PublicKey: p.PublicKey, 128 | 129 | // Apply the update only flag to every chunk to ensure 130 | // consistency between batches when the kernel module processes 131 | // them. 132 | UpdateOnly: p.UpdateOnly, 133 | 134 | // It'd be a bit weird to have a remove peer message with many 135 | // IPs, but just in case, add this to every peer's message. 136 | Remove: p.Remove, 137 | 138 | // The IPs for this chunk. 139 | AllowedIPs: tmp, 140 | } 141 | 142 | // Only pass certain fields on the first occurrence of a peer, so 143 | // that subsequent IPs won't be wiped out and space isn't wasted. 144 | if _, ok := knownPeers[p.PublicKey]; !ok { 145 | knownPeers[p.PublicKey] = struct{}{} 146 | 147 | pcfg.PresharedKey = p.PresharedKey 148 | pcfg.Endpoint = p.Endpoint 149 | pcfg.PersistentKeepaliveInterval = p.PersistentKeepaliveInterval 150 | 151 | // Important: do not move or appending peers won't work. 152 | pcfg.ReplaceAllowedIPs = p.ReplaceAllowedIPs 153 | } 154 | 155 | // Add a peer configuration to this batch and keep going. 156 | batch.Peers = []wgtypes.PeerConfig{pcfg} 157 | batches = append(batches, batch) 158 | } 159 | } 160 | 161 | // Do not allow peer replacement beyond the first message in a batch, 162 | // so we don't overwrite our previous batch work. 163 | for i := range batches { 164 | if i > 0 { 165 | batches[i].ReplacePeers = false 166 | } 167 | } 168 | 169 | return batches 170 | } 171 | 172 | // encodePeer returns a function to encode PeerConfig nested attributes. 173 | func encodePeer(p wgtypes.PeerConfig) func(ae *netlink.AttributeEncoder) error { 174 | return func(ae *netlink.AttributeEncoder) error { 175 | ae.Bytes(unix.WGPEER_A_PUBLIC_KEY, p.PublicKey[:]) 176 | 177 | // Flags are stored in a single attribute. 178 | var flags uint32 179 | if p.Remove { 180 | flags |= unix.WGPEER_F_REMOVE_ME 181 | } 182 | if p.ReplaceAllowedIPs { 183 | flags |= unix.WGPEER_F_REPLACE_ALLOWEDIPS 184 | } 185 | if p.UpdateOnly { 186 | flags |= unix.WGPEER_F_UPDATE_ONLY 187 | } 188 | if flags != 0 { 189 | ae.Uint32(unix.WGPEER_A_FLAGS, flags) 190 | } 191 | 192 | if p.PresharedKey != nil { 193 | ae.Bytes(unix.WGPEER_A_PRESHARED_KEY, (*p.PresharedKey)[:]) 194 | } 195 | 196 | if p.Endpoint != nil { 197 | ae.Do(unix.WGPEER_A_ENDPOINT, encodeSockaddr(*p.Endpoint)) 198 | } 199 | 200 | if p.PersistentKeepaliveInterval != nil { 201 | ae.Uint16(unix.WGPEER_A_PERSISTENT_KEEPALIVE_INTERVAL, uint16(p.PersistentKeepaliveInterval.Seconds())) 202 | } 203 | 204 | // Only apply allowed IPs if necessary. 205 | if len(p.AllowedIPs) > 0 { 206 | ae.Nested(unix.WGPEER_A_ALLOWEDIPS, encodeAllowedIPs(p.AllowedIPs)) 207 | } 208 | 209 | return nil 210 | } 211 | } 212 | 213 | // encodeSockaddr returns a function which encodes a net.UDPAddr as raw 214 | // sockaddr_in or sockaddr_in6 bytes. 215 | func encodeSockaddr(endpoint net.UDPAddr) func() ([]byte, error) { 216 | return func() ([]byte, error) { 217 | if !isValidIP(endpoint.IP) { 218 | return nil, fmt.Errorf("wglinux: invalid endpoint IP: %s", endpoint.IP.String()) 219 | } 220 | 221 | // Is this an IPv6 address? 222 | if isIPv6(endpoint.IP) { 223 | var addr [16]byte 224 | copy(addr[:], endpoint.IP.To16()) 225 | 226 | sa := unix.RawSockaddrInet6{ 227 | Family: unix.AF_INET6, 228 | Port: sockaddrPort(endpoint.Port), 229 | Addr: addr, 230 | } 231 | 232 | return (*(*[unix.SizeofSockaddrInet6]byte)(unsafe.Pointer(&sa)))[:], nil 233 | } 234 | 235 | // IPv4 address handling. 236 | var addr [4]byte 237 | copy(addr[:], endpoint.IP.To4()) 238 | 239 | sa := unix.RawSockaddrInet4{ 240 | Family: unix.AF_INET, 241 | Port: sockaddrPort(endpoint.Port), 242 | Addr: addr, 243 | } 244 | 245 | return (*(*[unix.SizeofSockaddrInet4]byte)(unsafe.Pointer(&sa)))[:], nil 246 | } 247 | } 248 | 249 | // encodeAllowedIPs returns a function to encode allowed IP nested attributes. 250 | func encodeAllowedIPs(ipns []net.IPNet) func(ae *netlink.AttributeEncoder) error { 251 | return func(ae *netlink.AttributeEncoder) error { 252 | for i, ipn := range ipns { 253 | if !isValidIP(ipn.IP) { 254 | return fmt.Errorf("wglinux: invalid allowed IP: %s", ipn.IP.String()) 255 | } 256 | 257 | family := uint16(unix.AF_INET6) 258 | if !isIPv6(ipn.IP) { 259 | // Make sure address is 4 bytes if IPv4. 260 | family = unix.AF_INET 261 | ipn.IP = ipn.IP.To4() 262 | } 263 | 264 | // Netlink arrays use type as an array index. 265 | ae.Nested(uint16(i), func(nae *netlink.AttributeEncoder) error { 266 | nae.Uint16(unix.WGALLOWEDIP_A_FAMILY, family) 267 | nae.Bytes(unix.WGALLOWEDIP_A_IPADDR, ipn.IP) 268 | 269 | ones, _ := ipn.Mask.Size() 270 | nae.Uint8(unix.WGALLOWEDIP_A_CIDR_MASK, uint8(ones)) 271 | return nil 272 | }) 273 | } 274 | 275 | return nil 276 | } 277 | } 278 | 279 | // isValidIP determines if IP is a valid IPv4 or IPv6 address. 280 | func isValidIP(ip net.IP) bool { 281 | return ip.To16() != nil 282 | } 283 | 284 | // isIPv6 determines if IP is a valid IPv6 address. 285 | func isIPv6(ip net.IP) bool { 286 | return isValidIP(ip) && ip.To4() == nil 287 | } 288 | 289 | // sockaddrPort interprets port as a big endian uint16 for use passing sockaddr 290 | // structures to the kernel. 291 | func sockaddrPort(port int) uint16 { 292 | return binary.BigEndian.Uint16(nlenc.Uint16Bytes(uint16(port))) 293 | } 294 | -------------------------------------------------------------------------------- /internal/wglinux/parse_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package wglinux 5 | 6 | import ( 7 | "fmt" 8 | "net" 9 | "time" 10 | "unsafe" 11 | 12 | "github.com/mdlayher/genetlink" 13 | "github.com/mdlayher/netlink" 14 | "golang.org/x/sys/unix" 15 | "golang.zx2c4.com/wireguard/wgctrl/wgtypes" 16 | ) 17 | 18 | // parseDevice parses a Device from a slice of generic netlink messages, 19 | // automatically merging peer lists from subsequent messages into the Device 20 | // from the first message. 21 | func parseDevice(msgs []genetlink.Message) (*wgtypes.Device, error) { 22 | var first wgtypes.Device 23 | knownPeers := make(map[wgtypes.Key]int) 24 | 25 | for i, m := range msgs { 26 | d, err := parseDeviceLoop(m) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | if i == 0 { 32 | // First message contains our target device. 33 | first = *d 34 | 35 | // Gather the known peers so that we can merge 36 | // them later if needed 37 | for i := range first.Peers { 38 | knownPeers[first.Peers[i].PublicKey] = i 39 | } 40 | 41 | continue 42 | } 43 | 44 | // Any subsequent messages have their peer contents merged into the 45 | // first "target" message. 46 | mergeDevices(&first, d, knownPeers) 47 | } 48 | 49 | return &first, nil 50 | } 51 | 52 | // parseDeviceLoop parses a Device from a single generic netlink message. 53 | func parseDeviceLoop(m genetlink.Message) (*wgtypes.Device, error) { 54 | ad, err := netlink.NewAttributeDecoder(m.Data) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | d := wgtypes.Device{Type: wgtypes.LinuxKernel} 60 | for ad.Next() { 61 | switch ad.Type() { 62 | case unix.WGDEVICE_A_IFINDEX: 63 | // Ignored; interface index isn't exposed at all in the userspace 64 | // configuration protocol, and name is more friendly anyway. 65 | case unix.WGDEVICE_A_IFNAME: 66 | d.Name = ad.String() 67 | case unix.WGDEVICE_A_PRIVATE_KEY: 68 | ad.Do(parseKey(&d.PrivateKey)) 69 | case unix.WGDEVICE_A_PUBLIC_KEY: 70 | ad.Do(parseKey(&d.PublicKey)) 71 | case unix.WGDEVICE_A_LISTEN_PORT: 72 | d.ListenPort = int(ad.Uint16()) 73 | case unix.WGDEVICE_A_FWMARK: 74 | d.FirewallMark = int(ad.Uint32()) 75 | case unix.WGDEVICE_A_PEERS: 76 | // Netlink array of peers. 77 | // 78 | // Errors while parsing are propagated up to top-level ad.Err check. 79 | ad.Nested(func(nad *netlink.AttributeDecoder) error { 80 | // Initialize to the number of peers in this decoder and begin 81 | // handling nested Peer attributes. 82 | d.Peers = make([]wgtypes.Peer, 0, nad.Len()) 83 | for nad.Next() { 84 | nad.Nested(func(nnad *netlink.AttributeDecoder) error { 85 | d.Peers = append(d.Peers, parsePeer(nnad)) 86 | return nil 87 | }) 88 | } 89 | 90 | return nil 91 | }) 92 | } 93 | } 94 | 95 | if err := ad.Err(); err != nil { 96 | return nil, err 97 | } 98 | 99 | return &d, nil 100 | } 101 | 102 | // parseAllowedIPs parses a wgtypes.Peer from a netlink attribute payload. 103 | func parsePeer(ad *netlink.AttributeDecoder) wgtypes.Peer { 104 | var p wgtypes.Peer 105 | for ad.Next() { 106 | switch ad.Type() { 107 | case unix.WGPEER_A_PUBLIC_KEY: 108 | ad.Do(parseKey(&p.PublicKey)) 109 | case unix.WGPEER_A_PRESHARED_KEY: 110 | ad.Do(parseKey(&p.PresharedKey)) 111 | case unix.WGPEER_A_ENDPOINT: 112 | p.Endpoint = &net.UDPAddr{} 113 | ad.Do(parseSockaddr(p.Endpoint)) 114 | case unix.WGPEER_A_PERSISTENT_KEEPALIVE_INTERVAL: 115 | p.PersistentKeepaliveInterval = time.Duration(ad.Uint16()) * time.Second 116 | case unix.WGPEER_A_LAST_HANDSHAKE_TIME: 117 | ad.Do(parseTimespec(&p.LastHandshakeTime)) 118 | case unix.WGPEER_A_RX_BYTES: 119 | p.ReceiveBytes = int64(ad.Uint64()) 120 | case unix.WGPEER_A_TX_BYTES: 121 | p.TransmitBytes = int64(ad.Uint64()) 122 | case unix.WGPEER_A_ALLOWEDIPS: 123 | ad.Nested(parseAllowedIPs(&p.AllowedIPs)) 124 | case unix.WGPEER_A_PROTOCOL_VERSION: 125 | p.ProtocolVersion = int(ad.Uint32()) 126 | } 127 | } 128 | 129 | return p 130 | } 131 | 132 | // parseAllowedIPs parses a slice of net.IPNet from a netlink attribute payload. 133 | func parseAllowedIPs(ipns *[]net.IPNet) func(ad *netlink.AttributeDecoder) error { 134 | return func(ad *netlink.AttributeDecoder) error { 135 | // Initialize to the number of allowed IPs and begin iterating through 136 | // the netlink array to decode each one. 137 | *ipns = make([]net.IPNet, 0, ad.Len()) 138 | for ad.Next() { 139 | // Allowed IP nested attributes. 140 | ad.Nested(func(nad *netlink.AttributeDecoder) error { 141 | var ( 142 | ipn net.IPNet 143 | mask int 144 | family int 145 | ) 146 | 147 | for nad.Next() { 148 | switch nad.Type() { 149 | case unix.WGALLOWEDIP_A_IPADDR: 150 | nad.Do(parseAddr(&ipn.IP)) 151 | case unix.WGALLOWEDIP_A_CIDR_MASK: 152 | mask = int(nad.Uint8()) 153 | case unix.WGALLOWEDIP_A_FAMILY: 154 | family = int(nad.Uint16()) 155 | } 156 | } 157 | 158 | if err := nad.Err(); err != nil { 159 | return err 160 | } 161 | 162 | // The address family determines the correct number of bits in 163 | // the mask. 164 | switch family { 165 | case unix.AF_INET: 166 | ipn.Mask = net.CIDRMask(mask, 32) 167 | case unix.AF_INET6: 168 | ipn.Mask = net.CIDRMask(mask, 128) 169 | } 170 | 171 | *ipns = append(*ipns, ipn) 172 | return nil 173 | }) 174 | } 175 | 176 | return nil 177 | } 178 | } 179 | 180 | // parseKey parses a wgtypes.Key from a byte slice. 181 | func parseKey(key *wgtypes.Key) func(b []byte) error { 182 | return func(b []byte) error { 183 | k, err := wgtypes.NewKey(b) 184 | if err != nil { 185 | return err 186 | } 187 | 188 | *key = k 189 | return nil 190 | } 191 | } 192 | 193 | // parseAddr parses a net.IP from raw in_addr or in6_addr struct bytes. 194 | func parseAddr(ip *net.IP) func(b []byte) error { 195 | return func(b []byte) error { 196 | switch len(b) { 197 | case net.IPv4len, net.IPv6len: 198 | // Okay to convert directly to net.IP; memory layout is identical. 199 | *ip = make(net.IP, len(b)) 200 | copy(*ip, b) 201 | return nil 202 | default: 203 | return fmt.Errorf("wglinux: unexpected IP address size: %d", len(b)) 204 | } 205 | } 206 | } 207 | 208 | // parseSockaddr parses a *net.UDPAddr from raw sockaddr_in or sockaddr_in6 bytes. 209 | func parseSockaddr(endpoint *net.UDPAddr) func(b []byte) error { 210 | return func(b []byte) error { 211 | switch len(b) { 212 | case unix.SizeofSockaddrInet4: 213 | // IPv4 address parsing. 214 | sa := *(*unix.RawSockaddrInet4)(unsafe.Pointer(&b[0])) 215 | 216 | *endpoint = net.UDPAddr{ 217 | IP: net.IP(sa.Addr[:]).To4(), 218 | Port: int(sockaddrPort(int(sa.Port))), 219 | } 220 | 221 | return nil 222 | case unix.SizeofSockaddrInet6: 223 | // IPv6 address parsing. 224 | sa := *(*unix.RawSockaddrInet6)(unsafe.Pointer(&b[0])) 225 | 226 | *endpoint = net.UDPAddr{ 227 | IP: net.IP(sa.Addr[:]), 228 | Port: int(sockaddrPort(int(sa.Port))), 229 | } 230 | 231 | return nil 232 | default: 233 | return fmt.Errorf("wglinux: unexpected sockaddr size: %d", len(b)) 234 | } 235 | } 236 | } 237 | 238 | // timespec32 is a unix.Timespec with 32-bit integers. 239 | type timespec32 struct { 240 | Sec int32 241 | Nsec int32 242 | } 243 | 244 | // timespec64 is a unix.Timespec with 64-bit integers. 245 | type timespec64 struct { 246 | Sec int64 247 | Nsec int64 248 | } 249 | 250 | const ( 251 | sizeofTimespec32 = int(unsafe.Sizeof(timespec32{})) 252 | sizeofTimespec64 = int(unsafe.Sizeof(timespec64{})) 253 | ) 254 | 255 | // parseTimespec parses a time.Time from raw timespec bytes. 256 | func parseTimespec(t *time.Time) func(b []byte) error { 257 | return func(b []byte) error { 258 | // It would appear that WireGuard can return a __kernel_timespec which 259 | // uses 64-bit integers, even on 32-bit platforms. Clarification of this 260 | // behavior is being sought in: 261 | // https://lists.zx2c4.com/pipermail/wireguard/2019-April/004088.html. 262 | // 263 | // In the mean time, be liberal and accept 32-bit and 64-bit variants. 264 | var sec, nsec int64 265 | 266 | switch len(b) { 267 | case sizeofTimespec32: 268 | ts := *(*timespec32)(unsafe.Pointer(&b[0])) 269 | 270 | sec = int64(ts.Sec) 271 | nsec = int64(ts.Nsec) 272 | case sizeofTimespec64: 273 | ts := *(*timespec64)(unsafe.Pointer(&b[0])) 274 | 275 | sec = ts.Sec 276 | nsec = ts.Nsec 277 | default: 278 | return fmt.Errorf("wglinux: unexpected timespec size: %d bytes, expected 8 or 16 bytes", len(b)) 279 | } 280 | 281 | // Only set fields if UNIX timestamp value is greater than 0, so the 282 | // caller will see a zero-value time.Time otherwise. 283 | if sec > 0 || nsec > 0 { 284 | *t = time.Unix(sec, nsec) 285 | } 286 | 287 | return nil 288 | } 289 | } 290 | 291 | // mergeDevices merges Peer information from d into target. mergeDevices is 292 | // used to deal with multiple incoming netlink messages for the same device. 293 | func mergeDevices(target, d *wgtypes.Device, knownPeers map[wgtypes.Key]int) { 294 | for i := range d.Peers { 295 | // Peer is already known, append to it's allowed IP networks 296 | if peerIndex, ok := knownPeers[d.Peers[i].PublicKey]; ok { 297 | target.Peers[peerIndex].AllowedIPs = append(target.Peers[peerIndex].AllowedIPs, d.Peers[i].AllowedIPs...) 298 | } else { // New peer, add it to the target peers. 299 | target.Peers = append(target.Peers, d.Peers[i]) 300 | knownPeers[d.Peers[i].PublicKey] = len(target.Peers) - 1 301 | } 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /internal/wgopenbsd/client_openbsd_test.go: -------------------------------------------------------------------------------- 1 | //go:build openbsd 2 | // +build openbsd 3 | 4 | package wgopenbsd 5 | 6 | import ( 7 | "errors" 8 | "net" 9 | "os" 10 | "testing" 11 | "time" 12 | "unsafe" 13 | 14 | "github.com/google/go-cmp/cmp" 15 | "golang.org/x/sys/unix" 16 | "golang.zx2c4.com/wireguard/wgctrl/internal/wgopenbsd/internal/wgh" 17 | "golang.zx2c4.com/wireguard/wgctrl/internal/wgtest" 18 | "golang.zx2c4.com/wireguard/wgctrl/wgtypes" 19 | ) 20 | 21 | func TestClientDevices(t *testing.T) { 22 | // Fixed parameters for the test. 23 | const ( 24 | n = 2 25 | 26 | devA = "testwg0" 27 | devB = "testwg1" 28 | ) 29 | 30 | var ifgrCalls int 31 | ifgrFunc := func(ifg *wgh.Ifgroupreq) error { 32 | // Verify the caller is asking for WireGuard interface group members. 33 | if diff := cmp.Diff(ifGroupWG, ifg.Name); diff != "" { 34 | t.Fatalf("unexpected interface group (-want +got):\n%s", diff) 35 | } 36 | 37 | switch ifgrCalls { 38 | case 0: 39 | // Inform the caller that we have n device names available. 40 | ifg.Len = n * wgh.SizeofIfgreq 41 | case 1: 42 | // The structure pointed at is the first in an array. Populate the 43 | // array memory with device names. 44 | *(*[n]wgh.Ifgreq)(unsafe.Pointer(ifg.Groups)) = [n]wgh.Ifgreq{ 45 | {Ifgrqu: devName(devA)}, 46 | {Ifgrqu: devName(devB)}, 47 | } 48 | default: 49 | t.Fatal("too many calls to ioctlIfgroupreq") 50 | } 51 | 52 | ifgrCalls++ 53 | return nil 54 | } 55 | 56 | // TODO(mdlayher): add a test case where the data.Size field changes between 57 | // call 1 and 2, so the caller must loop again to determine how much memory 58 | // to allocate for the memory slice. 59 | 60 | var wgIOCalls int 61 | wgDataIOFunc := func(data *wgh.WGDataIO) error { 62 | // Expect two calls per device, where the first call indicates the 63 | // number of bytes to populate, and the second would normally populate 64 | // the caller's memory. 65 | switch wgIOCalls { 66 | case 0, 2: 67 | data.Size = wgh.SizeofWGInterfaceIO 68 | case 1, 3: 69 | // No-op, nothing to fill out. 70 | default: 71 | t.Fatal("too many calls to ioctlWGDataIO") 72 | } 73 | 74 | wgIOCalls++ 75 | return nil 76 | } 77 | 78 | c := &Client{ 79 | ioctlIfgroupreq: ifgrFunc, 80 | ioctlWGDataIO: wgDataIOFunc, 81 | } 82 | 83 | devices, err := c.Devices() 84 | if err != nil { 85 | t.Fatalf("failed to get devices: %v", err) 86 | } 87 | 88 | // This test does basic sanity checking for fetching many devices. Other 89 | // tests will handle more complex cases. 90 | want := []*wgtypes.Device{ 91 | { 92 | Name: devA, 93 | Type: wgtypes.OpenBSDKernel, 94 | Peers: []wgtypes.Peer{}, 95 | }, 96 | { 97 | Name: devB, 98 | Type: wgtypes.OpenBSDKernel, 99 | Peers: []wgtypes.Peer{}, 100 | }, 101 | } 102 | 103 | if diff := cmp.Diff(want, devices); diff != "" { 104 | t.Fatalf("unexpected devices (-want +got):\n%s", diff) 105 | } 106 | } 107 | 108 | func TestClientDeviceBasic(t *testing.T) { 109 | // Fixed parameters for the test. 110 | const device = "testwg0" 111 | 112 | var ( 113 | priv = wgtest.MustPrivateKey() 114 | pub = priv.PublicKey() 115 | peerA = wgtest.MustPublicKey() 116 | peerB = wgtest.MustPublicKey() 117 | peerC = wgtest.MustPublicKey() 118 | psk = wgtest.MustPresharedKey() 119 | ) 120 | 121 | var calls int 122 | c := &Client{ 123 | ioctlIfgroupreq: func(_ *wgh.Ifgroupreq) error { 124 | panic("no calls to Client.Devices, should not be called") 125 | }, 126 | ioctlWGDataIO: func(data *wgh.WGDataIO) error { 127 | // Verify the caller is asking for WireGuard interface group members. 128 | if diff := cmp.Diff(devName(device), data.Name); diff != "" { 129 | t.Fatalf("unexpected interface name (-want +got):\n%s", diff) 130 | } 131 | 132 | switch calls { 133 | case 0: 134 | // Inform the caller that we have one device, one peer, and 135 | // two allowed IPs associated with that peer. 136 | data.Size = wgh.SizeofWGInterfaceIO + 137 | wgh.SizeofWGPeerIO + 2*wgh.SizeofWGAIPIO + 138 | wgh.SizeofWGPeerIO + wgh.SizeofWGAIPIO + 139 | wgh.SizeofWGPeerIO 140 | case 1: 141 | // The caller expects a WGInterfaceIO which is populated with 142 | // data, so fill it out now. 143 | b := pack( 144 | &wgh.WGInterfaceIO{ 145 | Flags: wgh.WG_INTERFACE_HAS_PUBLIC | 146 | wgh.WG_INTERFACE_HAS_PRIVATE | 147 | wgh.WG_INTERFACE_HAS_PORT | 148 | wgh.WG_INTERFACE_HAS_RTABLE, 149 | Port: 8080, 150 | Rtable: 1, 151 | Public: pub, 152 | Private: priv, 153 | Peers_count: 3, 154 | }, 155 | &wgh.WGPeerIO{ 156 | Flags: wgh.WG_PEER_HAS_PUBLIC | 157 | wgh.WG_PEER_HAS_PSK | 158 | wgh.WG_PEER_HAS_PKA | 159 | wgh.WG_PEER_HAS_ENDPOINT, 160 | Protocol_version: 1, 161 | Public: peerA, 162 | Psk: psk, 163 | Pka: 60, 164 | Endpoint: *(*[28]byte)(unsafe.Pointer(&unix.RawSockaddrInet4{ 165 | Len: uint8(unsafe.Sizeof(unix.RawSockaddrInet4{})), 166 | Family: unix.AF_INET, 167 | Port: uint16(bePort(1024)), 168 | Addr: [4]byte{192, 0, 2, 0}, 169 | })), 170 | Txbytes: 1, 171 | Rxbytes: 2, 172 | Last_handshake: wgh.Timespec{ 173 | Sec: 1, 174 | Nsec: 2, 175 | }, 176 | Aips_count: 2, 177 | }, 178 | &wgh.WGAIPIO{ 179 | Af: unix.AF_INET, 180 | Cidr: 24, 181 | Addr: [16]byte{0: 192, 1: 168, 2: 1, 3: 0}, 182 | }, 183 | &wgh.WGAIPIO{ 184 | Af: unix.AF_INET6, 185 | Cidr: 64, 186 | Addr: [16]byte{0: 0xfd}, 187 | }, 188 | &wgh.WGPeerIO{ 189 | Flags: wgh.WG_PEER_HAS_PUBLIC | 190 | wgh.WG_PEER_HAS_ENDPOINT, 191 | Public: peerB, 192 | Endpoint: *(*[28]byte)(unsafe.Pointer(&unix.RawSockaddrInet6{ 193 | Len: uint8(unsafe.Sizeof(unix.RawSockaddrInet6{})), 194 | Family: unix.AF_INET6, 195 | Port: uint16(bePort(2048)), 196 | Addr: [16]byte{15: 0x01}, 197 | })), 198 | Aips_count: 1, 199 | }, 200 | &wgh.WGAIPIO{ 201 | Af: unix.AF_INET6, 202 | Cidr: 128, 203 | Addr: [16]byte{0: 0x20, 1: 0x01, 2: 0x0d, 3: 0xb8, 15: 0x01}, 204 | }, 205 | &wgh.WGPeerIO{ 206 | Flags: wgh.WG_PEER_HAS_PUBLIC, 207 | Public: peerC, 208 | }, 209 | ) 210 | 211 | data.Interface = (*wgh.WGInterfaceIO)(unsafe.Pointer(&b[0])) 212 | default: 213 | t.Fatal("too many calls to ioctlWGDataIO") 214 | } 215 | 216 | calls++ 217 | return nil 218 | }, 219 | } 220 | 221 | d, err := c.Device(device) 222 | if err != nil { 223 | t.Fatalf("failed to get device: %v", err) 224 | } 225 | 226 | want := &wgtypes.Device{ 227 | Name: device, 228 | Type: wgtypes.OpenBSDKernel, 229 | PrivateKey: priv, 230 | PublicKey: pub, 231 | ListenPort: 8080, 232 | FirewallMark: 1, 233 | Peers: []wgtypes.Peer{ 234 | { 235 | PublicKey: peerA, 236 | PresharedKey: psk, 237 | Endpoint: wgtest.MustUDPAddr("192.0.2.0:1024"), 238 | PersistentKeepaliveInterval: 60 * time.Second, 239 | ReceiveBytes: 2, 240 | TransmitBytes: 1, 241 | LastHandshakeTime: time.Unix(1, 2), 242 | AllowedIPs: []net.IPNet{ 243 | wgtest.MustCIDR("192.168.1.0/24"), 244 | wgtest.MustCIDR("fd00::/64"), 245 | }, 246 | ProtocolVersion: 1, 247 | }, 248 | { 249 | PublicKey: peerB, 250 | Endpoint: wgtest.MustUDPAddr("[::1]:2048"), 251 | AllowedIPs: []net.IPNet{wgtest.MustCIDR("2001:db8::1/128")}, 252 | }, 253 | { 254 | PublicKey: peerC, 255 | AllowedIPs: []net.IPNet{}, 256 | }, 257 | }, 258 | } 259 | 260 | if diff := cmp.Diff(want, d); diff != "" { 261 | t.Fatalf("unexpected device (-want +got):\n%s", diff) 262 | } 263 | } 264 | 265 | func TestClientDeviceNotExist(t *testing.T) { 266 | tests := []struct { 267 | name string 268 | err error 269 | }{ 270 | { 271 | name: "ENXIO", 272 | err: os.NewSyscallError("ioctl", unix.ENXIO), 273 | }, 274 | { 275 | name: "ENOTTY", 276 | err: os.NewSyscallError("ioctl", unix.ENOTTY), 277 | }, 278 | } 279 | 280 | for _, tt := range tests { 281 | t.Run(tt.name, func(t *testing.T) { 282 | c := &Client{ 283 | ioctlWGDataIO: func(_ *wgh.WGDataIO) error { 284 | return tt.err 285 | }, 286 | } 287 | 288 | if _, err := c.Device("wgnotexist0"); !errors.Is(err, os.ErrNotExist) { 289 | t.Fatalf("expected is not exist, but got: %v", err) 290 | } 291 | }) 292 | } 293 | } 294 | 295 | func TestClientDeviceWrongMemorySize(t *testing.T) { 296 | c := &Client{ 297 | ioctlWGDataIO: func(data *wgh.WGDataIO) error { 298 | // Pass a nonsensical number of bytes back to the caller. 299 | data.Size = 1 300 | return nil 301 | }, 302 | } 303 | 304 | _, err := c.Device("wg0") 305 | if err == nil { 306 | t.Fatal("expected an error, but none occurred") 307 | } 308 | 309 | t.Logf("err: %v", err) 310 | } 311 | 312 | // pack packs a WGInterfaceIO and trailing WGPeerIO/WGAIPIO values in a 313 | // contiguous byte slice to emulate the kernel module output. 314 | func pack(ifio *wgh.WGInterfaceIO, values ...interface{}) []byte { 315 | out := (*(*[wgh.SizeofWGInterfaceIO]byte)(unsafe.Pointer(ifio)))[:] 316 | 317 | for _, v := range values { 318 | switch v := v.(type) { 319 | case *wgh.WGPeerIO: 320 | b := (*(*[wgh.SizeofWGPeerIO]byte)(unsafe.Pointer(v)))[:] 321 | out = append(out, b...) 322 | case *wgh.WGAIPIO: 323 | b := (*(*[wgh.SizeofWGAIPIO]byte)(unsafe.Pointer(v)))[:] 324 | out = append(out, b...) 325 | default: 326 | panicf("pack: invalid type %T", v) 327 | } 328 | } 329 | 330 | return out 331 | } 332 | 333 | func devName(name string) [16]byte { 334 | nb, err := deviceName(name) 335 | if err != nil { 336 | panicf("failed to make device name bytes: %v", err) 337 | } 338 | 339 | return nb 340 | } 341 | -------------------------------------------------------------------------------- /internal/wgwindows/client_windows.go: -------------------------------------------------------------------------------- 1 | package wgwindows 2 | 3 | import ( 4 | "net" 5 | "os" 6 | "time" 7 | "unsafe" 8 | 9 | "golang.org/x/sys/windows" 10 | 11 | "golang.zx2c4.com/wireguard/wgctrl/internal/wginternal" 12 | "golang.zx2c4.com/wireguard/wgctrl/internal/wgwindows/internal/ioctl" 13 | "golang.zx2c4.com/wireguard/wgctrl/wgtypes" 14 | ) 15 | 16 | var _ wginternal.Client = &Client{} 17 | 18 | // A Client provides access to WireGuardNT ioctl information. 19 | type Client struct { 20 | cachedInterfaces map[string]*uint16 21 | lastLenGuess uint32 22 | } 23 | 24 | var ( 25 | deviceClassNetGUID = windows.GUID{0x4d36e972, 0xe325, 0x11ce, [8]byte{0xbf, 0xc1, 0x08, 0x00, 0x2b, 0xe1, 0x03, 0x18}} 26 | deviceInterfaceNetGUID = windows.GUID{0xcac88484, 0x7515, 0x4c03, [8]byte{0x82, 0xe6, 0x71, 0xa8, 0x7a, 0xba, 0xc3, 0x61}} 27 | devpkeyWgName = windows.DEVPROPKEY{ 28 | FmtID: windows.DEVPROPGUID{0x65726957, 0x7547, 0x7261, [8]byte{0x64, 0x4e, 0x61, 0x6d, 0x65, 0x4b, 0x65, 0x79}}, 29 | PID: windows.DEVPROPID_FIRST_USABLE + 1, 30 | } 31 | ) 32 | 33 | var enumerator = `SWD\WireGuard` 34 | 35 | func init() { 36 | if maj, min, _ := windows.RtlGetNtVersionNumbers(); (maj == 6 && min <= 1) || maj < 6 { 37 | enumerator = `ROOT\WIREGUARD` 38 | } 39 | } 40 | 41 | func (c *Client) refreshInterfaceCache() error { 42 | cachedInterfaces := make(map[string]*uint16, 5) 43 | devInfo, err := windows.SetupDiGetClassDevsEx(&deviceClassNetGUID, enumerator, 0, windows.DIGCF_PRESENT, 0, "") 44 | if err != nil { 45 | return err 46 | } 47 | defer windows.SetupDiDestroyDeviceInfoList(devInfo) 48 | for i := 0; ; i++ { 49 | devInfoData, err := windows.SetupDiEnumDeviceInfo(devInfo, i) 50 | if err != nil { 51 | if err == windows.ERROR_NO_MORE_ITEMS { 52 | break 53 | } 54 | continue 55 | } 56 | prop, err := windows.SetupDiGetDeviceProperty(devInfo, devInfoData, &devpkeyWgName) 57 | if err != nil { 58 | continue 59 | } 60 | adapterName, ok := prop.(string) 61 | if !ok { 62 | continue 63 | } 64 | var status, problemCode uint32 65 | ret := windows.CM_Get_DevNode_Status(&status, &problemCode, devInfoData.DevInst, 0) 66 | if ret != nil || status&(windows.DN_DRIVER_LOADED|windows.DN_STARTED) != windows.DN_DRIVER_LOADED|windows.DN_STARTED { 67 | continue 68 | } 69 | instanceId, err := windows.SetupDiGetDeviceInstanceId(devInfo, devInfoData) 70 | if err != nil { 71 | continue 72 | } 73 | interfaces, err := windows.CM_Get_Device_Interface_List(instanceId, &deviceInterfaceNetGUID, windows.CM_GET_DEVICE_INTERFACE_LIST_PRESENT) 74 | if err != nil { 75 | continue 76 | } 77 | interface16, err := windows.UTF16PtrFromString(interfaces[0]) 78 | if err != nil { 79 | continue 80 | } 81 | cachedInterfaces[adapterName] = interface16 82 | } 83 | c.cachedInterfaces = cachedInterfaces 84 | return nil 85 | } 86 | 87 | func (c *Client) interfaceHandle(name string) (handle windows.Handle, err error) { 88 | hasRefreshed := false 89 | for !hasRefreshed { 90 | fileName, ok := c.cachedInterfaces[name] 91 | if !ok { 92 | err := c.refreshInterfaceCache() 93 | if err != nil { 94 | return 0, err 95 | } 96 | hasRefreshed = true 97 | fileName, ok = c.cachedInterfaces[name] 98 | if !ok { 99 | return 0, os.ErrNotExist 100 | } 101 | } 102 | handle, err = windows.CreateFile(fileName, windows.GENERIC_READ|windows.GENERIC_WRITE, windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE, nil, windows.OPEN_EXISTING, 0, 0) 103 | if err == nil { 104 | break 105 | } 106 | if err == windows.ERROR_FILE_NOT_FOUND { 107 | return 0, err 108 | } 109 | } 110 | return 111 | } 112 | 113 | // Devices implements wginternal.Client. 114 | func (c *Client) Devices() ([]*wgtypes.Device, error) { 115 | err := c.refreshInterfaceCache() 116 | if err != nil { 117 | return nil, err 118 | } 119 | ds := make([]*wgtypes.Device, 0, len(c.cachedInterfaces)) 120 | for name := range c.cachedInterfaces { 121 | d, err := c.Device(name) 122 | if err != nil { 123 | return nil, err 124 | } 125 | ds = append(ds, d) 126 | } 127 | return ds, nil 128 | } 129 | 130 | // New creates a new Client 131 | func New() *Client { 132 | return &Client{} 133 | } 134 | 135 | // Close implements wginternal.Client. 136 | func (c *Client) Close() error { 137 | return nil 138 | } 139 | 140 | // Device implements wginternal.Client. 141 | func (c *Client) Device(name string) (*wgtypes.Device, error) { 142 | handle, err := c.interfaceHandle(name) 143 | if err != nil { 144 | return nil, err 145 | } 146 | defer windows.CloseHandle(handle) 147 | 148 | size := c.lastLenGuess 149 | if size == 0 { 150 | size = 512 151 | } 152 | var buf []byte 153 | for { 154 | buf = make([]byte, size) 155 | err = windows.DeviceIoControl(handle, ioctl.IoctlGet, nil, 0, &buf[0], size, &size, nil) 156 | if err == windows.ERROR_MORE_DATA { 157 | continue 158 | } 159 | if err != nil { 160 | return nil, err 161 | } 162 | break 163 | } 164 | c.lastLenGuess = size 165 | interfaze := (*ioctl.Interface)(unsafe.Pointer(&buf[0])) 166 | 167 | device := wgtypes.Device{Type: wgtypes.WindowsKernel, Name: name} 168 | if interfaze.Flags&ioctl.InterfaceHasPrivateKey != 0 { 169 | device.PrivateKey = interfaze.PrivateKey 170 | } 171 | if interfaze.Flags&ioctl.InterfaceHasPublicKey != 0 { 172 | device.PublicKey = interfaze.PublicKey 173 | } 174 | if interfaze.Flags&ioctl.InterfaceHasListenPort != 0 { 175 | device.ListenPort = int(interfaze.ListenPort) 176 | } 177 | var p *ioctl.Peer 178 | for i := uint32(0); i < interfaze.PeerCount; i++ { 179 | if p == nil { 180 | p = interfaze.FirstPeer() 181 | } else { 182 | p = p.NextPeer() 183 | } 184 | peer := wgtypes.Peer{} 185 | if p.Flags&ioctl.PeerHasPublicKey != 0 { 186 | peer.PublicKey = p.PublicKey 187 | } 188 | if p.Flags&ioctl.PeerHasPresharedKey != 0 { 189 | peer.PresharedKey = p.PresharedKey 190 | } 191 | if p.Flags&ioctl.PeerHasEndpoint != 0 { 192 | peer.Endpoint = &net.UDPAddr{IP: p.Endpoint.IP(), Port: int(p.Endpoint.Port())} 193 | } 194 | if p.Flags&ioctl.PeerHasPersistentKeepalive != 0 { 195 | peer.PersistentKeepaliveInterval = time.Duration(p.PersistentKeepalive) * time.Second 196 | } 197 | if p.Flags&ioctl.PeerHasProtocolVersion != 0 { 198 | peer.ProtocolVersion = int(p.ProtocolVersion) 199 | } 200 | peer.TransmitBytes = int64(p.TxBytes) 201 | peer.ReceiveBytes = int64(p.RxBytes) 202 | if p.LastHandshake != 0 { 203 | peer.LastHandshakeTime = time.Unix(0, int64((p.LastHandshake-116444736000000000)*100)) 204 | } 205 | var a *ioctl.AllowedIP 206 | for j := uint32(0); j < p.AllowedIPsCount; j++ { 207 | if a == nil { 208 | a = p.FirstAllowedIP() 209 | } else { 210 | a = a.NextAllowedIP() 211 | } 212 | var ip net.IP 213 | var bits int 214 | if a.AddressFamily == windows.AF_INET { 215 | ip = a.Address[:4] 216 | bits = 32 217 | } else if a.AddressFamily == windows.AF_INET6 { 218 | ip = a.Address[:16] 219 | bits = 128 220 | } 221 | peer.AllowedIPs = append(peer.AllowedIPs, net.IPNet{ 222 | IP: ip, 223 | Mask: net.CIDRMask(int(a.Cidr), bits), 224 | }) 225 | } 226 | device.Peers = append(device.Peers, peer) 227 | } 228 | return &device, nil 229 | } 230 | 231 | // ConfigureDevice implements wginternal.Client. 232 | func (c *Client) ConfigureDevice(name string, cfg wgtypes.Config) error { 233 | handle, err := c.interfaceHandle(name) 234 | if err != nil { 235 | return err 236 | } 237 | defer windows.CloseHandle(handle) 238 | 239 | preallocation := unsafe.Sizeof(ioctl.Interface{}) + uintptr(len(cfg.Peers))*unsafe.Sizeof(ioctl.Peer{}) 240 | for i := range cfg.Peers { 241 | preallocation += uintptr(len(cfg.Peers[i].AllowedIPs)) * unsafe.Sizeof(ioctl.AllowedIP{}) 242 | } 243 | var b ioctl.ConfigBuilder 244 | b.Preallocate(uint32(preallocation)) 245 | interfaze := &ioctl.Interface{PeerCount: uint32(len(cfg.Peers))} 246 | if cfg.ReplacePeers { 247 | interfaze.Flags |= ioctl.InterfaceReplacePeers 248 | } 249 | if cfg.PrivateKey != nil { 250 | interfaze.PrivateKey = *cfg.PrivateKey 251 | interfaze.Flags |= ioctl.InterfaceHasPrivateKey 252 | } 253 | if cfg.ListenPort != nil { 254 | interfaze.ListenPort = uint16(*cfg.ListenPort) 255 | interfaze.Flags |= ioctl.InterfaceHasListenPort 256 | } 257 | b.AppendInterface(interfaze) 258 | for i := range cfg.Peers { 259 | peer := &ioctl.Peer{ 260 | Flags: ioctl.PeerHasPublicKey, 261 | PublicKey: cfg.Peers[i].PublicKey, 262 | AllowedIPsCount: uint32(len(cfg.Peers[i].AllowedIPs)), 263 | } 264 | if cfg.Peers[i].ReplaceAllowedIPs { 265 | peer.Flags |= ioctl.PeerReplaceAllowedIPs 266 | } 267 | if cfg.Peers[i].UpdateOnly { 268 | peer.Flags |= ioctl.PeerUpdateOnly 269 | } 270 | if cfg.Peers[i].Remove { 271 | peer.Flags |= ioctl.PeerRemove 272 | } 273 | if cfg.Peers[i].PresharedKey != nil { 274 | peer.Flags |= ioctl.PeerHasPresharedKey 275 | peer.PresharedKey = *cfg.Peers[i].PresharedKey 276 | } 277 | if cfg.Peers[i].Endpoint != nil { 278 | peer.Flags |= ioctl.PeerHasEndpoint 279 | peer.Endpoint.SetIP(cfg.Peers[i].Endpoint.IP, uint16(cfg.Peers[i].Endpoint.Port)) 280 | } 281 | if cfg.Peers[i].PersistentKeepaliveInterval != nil { 282 | peer.Flags |= ioctl.PeerHasPersistentKeepalive 283 | peer.PersistentKeepalive = uint16(*cfg.Peers[i].PersistentKeepaliveInterval / time.Second) 284 | } 285 | b.AppendPeer(peer) 286 | for j := range cfg.Peers[i].AllowedIPs { 287 | var family ioctl.AddressFamily 288 | var ip net.IP 289 | if ip = cfg.Peers[i].AllowedIPs[j].IP.To4(); ip != nil { 290 | family = windows.AF_INET 291 | } else if ip = cfg.Peers[i].AllowedIPs[j].IP.To16(); ip != nil { 292 | family = windows.AF_INET6 293 | } else { 294 | ip = cfg.Peers[i].AllowedIPs[j].IP 295 | } 296 | cidr, _ := cfg.Peers[i].AllowedIPs[j].Mask.Size() 297 | a := &ioctl.AllowedIP{ 298 | AddressFamily: family, 299 | Cidr: uint8(cidr), 300 | } 301 | copy(a.Address[:], ip) 302 | b.AppendAllowedIP(a) 303 | } 304 | } 305 | interfaze, size := b.Interface() 306 | return windows.DeviceIoControl(handle, ioctl.IoctlSet, nil, 0, (*byte)(unsafe.Pointer(interfaze)), size, &size, nil) 307 | } 308 | -------------------------------------------------------------------------------- /client_integration_test.go: -------------------------------------------------------------------------------- 1 | package wgctrl_test 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "os" 9 | "sort" 10 | "strings" 11 | "testing" 12 | "time" 13 | 14 | "github.com/google/go-cmp/cmp" 15 | "github.com/mikioh/ipaddr" 16 | "golang.zx2c4.com/wireguard/wgctrl" 17 | "golang.zx2c4.com/wireguard/wgctrl/internal/wginternal" 18 | "golang.zx2c4.com/wireguard/wgctrl/internal/wgtest" 19 | "golang.zx2c4.com/wireguard/wgctrl/wgtypes" 20 | ) 21 | 22 | func TestIntegrationClient(t *testing.T) { 23 | c, done := integrationClient(t) 24 | defer done() 25 | 26 | devices, err := c.Devices() 27 | if err != nil { 28 | // It seems that not all errors returned by UNIX socket dialing 29 | // conform to os.IsPermission, so for now, be lenient and assume that 30 | // any error here means that permission was denied. 31 | t.Skipf("skipping, failed to get devices: %v", err) 32 | } 33 | 34 | tests := []struct { 35 | name string 36 | fn func(t *testing.T, c *wgctrl.Client, d *wgtypes.Device) 37 | }{ 38 | { 39 | name: "get", 40 | fn: testGet, 41 | }, 42 | { 43 | name: "configure", 44 | fn: testConfigure, 45 | }, 46 | { 47 | name: "configure many IPs", 48 | fn: testConfigureManyIPs, 49 | }, 50 | { 51 | name: "configure many peers", 52 | fn: testConfigureManyPeers, 53 | }, 54 | { 55 | name: "configure peers update only", 56 | fn: testConfigurePeersUpdateOnly, 57 | }, 58 | { 59 | name: "reset", 60 | fn: func(t *testing.T, c *wgctrl.Client, d *wgtypes.Device) { 61 | // Reset device several times; this used to cause a hang in 62 | // wireguard-go in late 2018. 63 | for i := 0; i < 10; i++ { 64 | resetDevice(t, c, d) 65 | } 66 | }, 67 | }, 68 | } 69 | 70 | for _, tt := range tests { 71 | t.Run(tt.name, func(t *testing.T) { 72 | // Panic if a specific test takes too long. 73 | timer := time.AfterFunc(5*time.Minute, func() { 74 | panic("test took too long") 75 | }) 76 | defer timer.Stop() 77 | 78 | for _, d := range devices { 79 | // Break each device into another sub-test so that individual 80 | // drivers can be implemented as read-only and skip configuration 81 | // and reset in tests. 82 | t.Run(d.Name, func(t *testing.T) { 83 | tt.fn(t, c, d) 84 | 85 | // Start with a clean state after each test. 86 | resetDevice(t, c, d) 87 | }) 88 | } 89 | }) 90 | } 91 | } 92 | 93 | func TestIntegrationClientIsNotExist(t *testing.T) { 94 | c, done := integrationClient(t) 95 | defer done() 96 | 97 | if _, err := c.Device("wgnotexist0"); !errors.Is(err, os.ErrNotExist) { 98 | t.Fatalf("expected is not exist error, but got: %v", err) 99 | } 100 | } 101 | 102 | func integrationClient(t *testing.T) (*wgctrl.Client, func()) { 103 | t.Helper() 104 | 105 | const ( 106 | env = "WGCTRL_INTEGRATION" 107 | confirm = "yesreallydoit" 108 | ) 109 | 110 | if os.Getenv(env) != confirm { 111 | t.Skipf("skipping, set '%s=%s to run; DANGER: this will reset and reconfigure any detected WireGuard devices", 112 | env, confirm) 113 | } 114 | 115 | c, err := wgctrl.New() 116 | if err != nil { 117 | if errors.Is(err, os.ErrNotExist) { 118 | t.Skip("skipping, wgctrl is not available on this system") 119 | } 120 | 121 | t.Fatalf("failed to open client: %v", err) 122 | } 123 | 124 | return c, func() { 125 | if err := c.Close(); err != nil { 126 | t.Fatalf("failed to close client: %v", err) 127 | } 128 | } 129 | } 130 | 131 | func testGet(t *testing.T, c *wgctrl.Client, d *wgtypes.Device) { 132 | t.Logf("device: %s: %s", d.Name, d.PublicKey.String()) 133 | 134 | dn, err := c.Device(d.Name) 135 | if err != nil { 136 | t.Fatalf("failed to get %q: %v", d.Name, err) 137 | } 138 | 139 | if diff := cmp.Diff(d, dn); diff != "" { 140 | t.Fatalf("unexpected Device (-want +got):\n%s", diff) 141 | } 142 | } 143 | 144 | func testConfigure(t *testing.T, c *wgctrl.Client, d *wgtypes.Device) { 145 | var ( 146 | port = 8888 147 | ips = []net.IPNet{ 148 | wgtest.MustCIDR("192.0.2.0/32"), 149 | wgtest.MustCIDR("2001:db8::/128"), 150 | } 151 | 152 | priv = wgtest.MustPrivateKey() 153 | peerKey = wgtest.MustPublicKey() 154 | ) 155 | 156 | t.Logf("before: %s: %s", d.Name, d.PublicKey.String()) 157 | 158 | cfg := wgtypes.Config{ 159 | PrivateKey: &priv, 160 | ListenPort: &port, 161 | ReplacePeers: true, 162 | Peers: []wgtypes.PeerConfig{{ 163 | PublicKey: peerKey, 164 | ReplaceAllowedIPs: true, 165 | AllowedIPs: ips, 166 | }}, 167 | } 168 | 169 | tryConfigure(t, c, d.Name, cfg) 170 | 171 | dn, err := c.Device(d.Name) 172 | if err != nil { 173 | t.Fatalf("failed to get %q by name: %v", d.Name, err) 174 | } 175 | 176 | // Now that a new configuration has been applied, update our initial 177 | // device for comparison. 178 | *d = wgtypes.Device{ 179 | Name: d.Name, 180 | Type: d.Type, 181 | PrivateKey: priv, 182 | PublicKey: priv.PublicKey(), 183 | ListenPort: port, 184 | Peers: []wgtypes.Peer{{ 185 | PublicKey: peerKey, 186 | LastHandshakeTime: time.Time{}, 187 | AllowedIPs: ips, 188 | ProtocolVersion: 1, 189 | }}, 190 | } 191 | 192 | // Sort AllowedIPs as different implementations might return 193 | // them in different order 194 | for i := range dn.Peers { 195 | ips := dn.Peers[i].AllowedIPs 196 | sort.Slice(ips, func(i, j int) bool { 197 | return bytes.Compare(ips[i].IP, ips[j].IP) > 0 198 | }) 199 | } 200 | 201 | if diff := cmp.Diff(d, dn); diff != "" { 202 | t.Fatalf("unexpected Device from Device (-want +got):\n%s", diff) 203 | } 204 | 205 | // Leading space for alignment. 206 | out := fmt.Sprintf(" after: %s: %s\n", dn.Name, dn.PublicKey.String()) 207 | for _, p := range dn.Peers { 208 | out += fmt.Sprintf("- peer: %s, IPs: %s\n", p.PublicKey.String(), ipsString(p.AllowedIPs)) 209 | } 210 | 211 | t.Log(out) 212 | } 213 | 214 | func testConfigureManyIPs(t *testing.T, c *wgctrl.Client, d *wgtypes.Device) { 215 | // Apply 511 IPs per peer. 216 | var ( 217 | countIPs int 218 | peers []wgtypes.PeerConfig 219 | ) 220 | 221 | for i := 0; i < 2; i++ { 222 | cidr := "2001:db8::/119" 223 | if i == 1 { 224 | cidr = "2001:db8:ffff::/119" 225 | } 226 | 227 | cur, err := ipaddr.Parse(cidr) 228 | if err != nil { 229 | t.Fatalf("failed to create cursor: %v", err) 230 | } 231 | 232 | var ips []net.IPNet 233 | for pos := cur.Next(); pos != nil; pos = cur.Next() { 234 | bits := 128 235 | if pos.IP.To4() != nil { 236 | bits = 32 237 | } 238 | 239 | ips = append(ips, net.IPNet{ 240 | IP: pos.IP, 241 | Mask: net.CIDRMask(bits, bits), 242 | }) 243 | } 244 | 245 | peers = append(peers, wgtypes.PeerConfig{ 246 | PublicKey: wgtest.MustPublicKey(), 247 | ReplaceAllowedIPs: true, 248 | AllowedIPs: ips, 249 | }) 250 | 251 | countIPs += len(ips) 252 | } 253 | 254 | cfg := wgtypes.Config{ 255 | ReplacePeers: true, 256 | Peers: peers, 257 | } 258 | 259 | tryConfigure(t, c, d.Name, cfg) 260 | 261 | dn, err := c.Device(d.Name) 262 | if err != nil { 263 | t.Fatalf("failed to get %q by name: %v", d.Name, err) 264 | } 265 | 266 | peerIPs := countPeerIPs(dn) 267 | if diff := cmp.Diff(countIPs, peerIPs); diff != "" { 268 | t.Fatalf("unexpected number of configured peer IPs (-want +got):\n%s", diff) 269 | } 270 | 271 | t.Logf("device: %s: %d IPs", d.Name, peerIPs) 272 | } 273 | 274 | func testConfigureManyPeers(t *testing.T, c *wgctrl.Client, d *wgtypes.Device) { 275 | const ( 276 | nPeers = 256 277 | peerIPs = 512 278 | ) 279 | 280 | var peers []wgtypes.PeerConfig 281 | for i := 0; i < nPeers; i++ { 282 | var ( 283 | pk = wgtest.MustPresharedKey() 284 | dur = 10 * time.Second 285 | ) 286 | 287 | ips := generateIPs((i + 1) * 2) 288 | 289 | peers = append(peers, wgtypes.PeerConfig{ 290 | PublicKey: wgtest.MustPublicKey(), 291 | PresharedKey: &pk, 292 | ReplaceAllowedIPs: true, 293 | Endpoint: &net.UDPAddr{ 294 | IP: ips[0].IP, 295 | Port: 1111, 296 | }, 297 | PersistentKeepaliveInterval: &dur, 298 | AllowedIPs: ips, 299 | }) 300 | } 301 | 302 | var ( 303 | priv = wgtest.MustPrivateKey() 304 | n = 0 305 | ) 306 | 307 | cfg := wgtypes.Config{ 308 | PrivateKey: &priv, 309 | ListenPort: &n, 310 | FirewallMark: &n, 311 | ReplacePeers: true, 312 | Peers: peers, 313 | } 314 | 315 | tryConfigure(t, c, d.Name, cfg) 316 | 317 | dn, err := c.Device(d.Name) 318 | if err != nil { 319 | t.Fatalf("failed to get updated device: %v", err) 320 | } 321 | 322 | if diff := cmp.Diff(nPeers, len(dn.Peers)); diff != "" { 323 | t.Fatalf("unexpected number of peers (-want +got):\n%s", diff) 324 | } 325 | 326 | countIPs := countPeerIPs(dn) 327 | if diff := cmp.Diff(peerIPs, countIPs); diff != "" { 328 | t.Fatalf("unexpected number of peer IPs (-want +got):\n%s", diff) 329 | } 330 | 331 | t.Logf("device: %s: %d peers, %d IPs", d.Name, len(dn.Peers), countIPs) 332 | } 333 | 334 | func testConfigurePeersUpdateOnly(t *testing.T, c *wgctrl.Client, d *wgtypes.Device) { 335 | var ( 336 | peerA = wgtest.MustPublicKey() 337 | peerB = wgtest.MustPublicKey() 338 | psk = wgtest.MustPresharedKey() 339 | ) 340 | 341 | // Create an initial peer configuration. 342 | cfg := wgtypes.Config{ 343 | Peers: []wgtypes.PeerConfig{{ 344 | PublicKey: peerA, 345 | }}, 346 | } 347 | 348 | tryConfigure(t, c, d.Name, cfg) 349 | 350 | // Create an updated configuration that should only apply to the existing 351 | // peer due to update only flags. 352 | cfg = wgtypes.Config{ 353 | Peers: []wgtypes.PeerConfig{ 354 | { 355 | PublicKey: peerA, 356 | UpdateOnly: true, 357 | PresharedKey: &psk, 358 | }, 359 | { 360 | PublicKey: peerB, 361 | UpdateOnly: true, 362 | PresharedKey: &psk, 363 | }, 364 | }, 365 | } 366 | 367 | if err := c.ConfigureDevice(d.Name, cfg); err != nil { 368 | if d.Type == wgtypes.FreeBSDKernel && err == wgtypes.ErrUpdateOnlyNotSupported { 369 | // TODO(stv0g): remove as soon as the FreeBSD kernel module supports it 370 | t.Skip("FreeBSD kernel devices do not support UpdateOnly flag") 371 | } 372 | 373 | 374 | t.Fatalf("failed to configure second time on %q: %v", d.Name, err) 375 | } 376 | 377 | dn, err := c.Device(d.Name) 378 | if err != nil { 379 | t.Fatalf("failed to get updated device: %v", err) 380 | } 381 | 382 | want := []wgtypes.Peer{{ 383 | PublicKey: peerA, 384 | PresharedKey: psk, 385 | ProtocolVersion: 1, 386 | }} 387 | 388 | if diff := cmp.Diff(want, dn.Peers); diff != "" { 389 | t.Fatalf("unexpected configured peers (-want +got):\n%s", diff) 390 | } 391 | } 392 | 393 | func resetDevice(t *testing.T, c *wgctrl.Client, d *wgtypes.Device) { 394 | t.Helper() 395 | 396 | zero := 0 397 | cfg := wgtypes.Config{ 398 | // Clear device config. 399 | PrivateKey: &wgtypes.Key{}, 400 | ListenPort: &zero, 401 | FirewallMark: &zero, 402 | 403 | // Clear all peers. 404 | ReplacePeers: true, 405 | } 406 | 407 | tryConfigure(t, c, d.Name, cfg) 408 | } 409 | 410 | func tryConfigure(t *testing.T, c *wgctrl.Client, device string, cfg wgtypes.Config) { 411 | t.Helper() 412 | 413 | if err := c.ConfigureDevice(device, cfg); err != nil { 414 | if err == wginternal.ErrReadOnly { 415 | t.Skipf("skipping, device %q implementation is read-only", device) 416 | } 417 | 418 | t.Fatalf("failed to configure %q: %v", device, err) 419 | } 420 | } 421 | 422 | func countPeerIPs(d *wgtypes.Device) int { 423 | var count int 424 | for _, p := range d.Peers { 425 | count += len(p.AllowedIPs) 426 | } 427 | 428 | return count 429 | } 430 | 431 | func ipsString(ipns []net.IPNet) string { 432 | ss := make([]string, 0, len(ipns)) 433 | for _, ipn := range ipns { 434 | ss = append(ss, ipn.String()) 435 | } 436 | 437 | return strings.Join(ss, ", ") 438 | } 439 | 440 | func generateIPs(n int) []net.IPNet { 441 | cur, err := ipaddr.Parse("2001:db8::/64") 442 | if err != nil { 443 | panicf("failed to create cursor: %v", err) 444 | } 445 | 446 | ips := make([]net.IPNet, 0, n) 447 | for i := 0; i < n; i++ { 448 | pos := cur.Next() 449 | if pos == nil { 450 | panic("hit nil IP during IP generation") 451 | } 452 | 453 | ips = append(ips, net.IPNet{ 454 | IP: pos.IP, 455 | Mask: net.CIDRMask(128, 128), 456 | }) 457 | } 458 | 459 | return ips 460 | } 461 | 462 | func panicf(format string, a ...interface{}) { 463 | panic(fmt.Sprintf(format, a...)) 464 | } 465 | -------------------------------------------------------------------------------- /internal/wgopenbsd/client_openbsd.go: -------------------------------------------------------------------------------- 1 | //go:build openbsd 2 | // +build openbsd 3 | 4 | package wgopenbsd 5 | 6 | import ( 7 | "bytes" 8 | "encoding/binary" 9 | "fmt" 10 | "net" 11 | "os" 12 | "runtime" 13 | "time" 14 | "unsafe" 15 | 16 | "golang.org/x/sys/unix" 17 | "golang.zx2c4.com/wireguard/wgctrl/internal/wginternal" 18 | "golang.zx2c4.com/wireguard/wgctrl/internal/wgopenbsd/internal/wgh" 19 | "golang.zx2c4.com/wireguard/wgctrl/wgtypes" 20 | ) 21 | 22 | // ifGroupWG is the WireGuard interface group name passed to the kernel. 23 | var ifGroupWG = [16]byte{0: 'w', 1: 'g'} 24 | 25 | var _ wginternal.Client = &Client{} 26 | 27 | // A Client provides access to OpenBSD WireGuard ioctl information. 28 | type Client struct { 29 | // Hooks which use system calls by default, but can also be swapped out 30 | // during tests. 31 | close func() error 32 | ioctlIfgroupreq func(ifg *wgh.Ifgroupreq) error 33 | ioctlWGDataIO func(data *wgh.WGDataIO) error 34 | } 35 | 36 | // New creates a new Client and returns whether or not the ioctl interface 37 | // is available. 38 | func New() (*Client, bool, error) { 39 | // The OpenBSD ioctl interface operates on a generic AF_INET socket. 40 | fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0) 41 | if err != nil { 42 | return nil, false, err 43 | } 44 | 45 | // TODO(mdlayher): find a call to invoke here to probe for availability. 46 | // c.Devices won't work because it returns a "not found" error when the 47 | // kernel WireGuard implementation is available but the interface group 48 | // has no members. 49 | 50 | // By default, use system call implementations for all hook functions. 51 | return &Client{ 52 | close: func() error { return unix.Close(fd) }, 53 | ioctlIfgroupreq: ioctlIfgroupreq(fd), 54 | ioctlWGDataIO: ioctlWGDataIO(fd), 55 | }, true, nil 56 | } 57 | 58 | // Close implements wginternal.Client. 59 | func (c *Client) Close() error { 60 | return c.close() 61 | } 62 | 63 | // Devices implements wginternal.Client. 64 | func (c *Client) Devices() ([]*wgtypes.Device, error) { 65 | ifg := wgh.Ifgroupreq{ 66 | // Query for devices in the "wg" group. 67 | Name: ifGroupWG, 68 | } 69 | 70 | // Determine how many device names we must allocate memory for. 71 | if err := c.ioctlIfgroupreq(&ifg); err != nil { 72 | return nil, err 73 | } 74 | 75 | // ifg.Len is size in bytes; allocate enough memory for the correct number 76 | // of wgh.Ifgreq and then store a pointer to the memory where the data 77 | // should be written (ifgrs) in ifg.Groups. 78 | // 79 | // From a thread in golang-nuts, this pattern is valid: 80 | // "It would be OK to pass a pointer to a struct to ioctl if the struct 81 | // contains a pointer to other Go memory, but the struct field must have 82 | // pointer type." 83 | // See: https://groups.google.com/forum/#!topic/golang-nuts/FfasFTZvU_o. 84 | ifgrs := make([]wgh.Ifgreq, ifg.Len/wgh.SizeofIfgreq) 85 | ifg.Groups = &ifgrs[0] 86 | 87 | // Now actually fetch the device names. 88 | if err := c.ioctlIfgroupreq(&ifg); err != nil { 89 | return nil, err 90 | } 91 | 92 | // Keep this alive until we're done doing the ioctl dance. 93 | runtime.KeepAlive(&ifg) 94 | 95 | devices := make([]*wgtypes.Device, 0, len(ifgrs)) 96 | for _, ifgr := range ifgrs { 97 | // Remove any trailing NULL bytes from the interface names. 98 | d, err := c.Device(string(bytes.TrimRight(ifgr.Ifgrqu[:], "\x00"))) 99 | if err != nil { 100 | return nil, err 101 | } 102 | 103 | devices = append(devices, d) 104 | } 105 | 106 | return devices, nil 107 | } 108 | 109 | // Device implements wginternal.Client. 110 | func (c *Client) Device(name string) (*wgtypes.Device, error) { 111 | dname, err := deviceName(name) 112 | if err != nil { 113 | return nil, err 114 | } 115 | 116 | // First, specify the name of the device and determine how much memory 117 | // must be allocated in order to store the WGInterfaceIO structure and 118 | // any trailing WGPeerIO/WGAIPIOs. 119 | data := wgh.WGDataIO{Name: dname} 120 | 121 | // TODO: consider preallocating some memory to avoid a second system call 122 | // if it proves to be a concern. 123 | var mem []byte 124 | for { 125 | if err := c.ioctlWGDataIO(&data); err != nil { 126 | // ioctl functions always return a wrapped unix.Errno value. 127 | // Conform to the wgctrl contract by unwrapping some values: 128 | // ENXIO: "no such device": (no such WireGuard device) 129 | // ENOTTY: "inappropriate ioctl for device" (device is not a 130 | // WireGuard device) 131 | switch err.(*os.SyscallError).Err { 132 | case unix.ENXIO, unix.ENOTTY: 133 | return nil, os.ErrNotExist 134 | default: 135 | return nil, err 136 | } 137 | } 138 | 139 | if len(mem) >= int(data.Size) { 140 | // Allocated enough memory! 141 | break 142 | } 143 | 144 | // Ensure we don't unsafe cast into uninitialized memory. We need at very 145 | // least a single WGInterfaceIO with no peers. 146 | if data.Size < wgh.SizeofWGInterfaceIO { 147 | return nil, fmt.Errorf("wgopenbsd: kernel returned unexpected number of bytes for WGInterfaceIO: %d", data.Size) 148 | } 149 | 150 | // Allocate the appropriate amount of memory and point the kernel at 151 | // the first byte of our slice's backing array. When the loop continues, 152 | // we will check if we've allocated enough memory. 153 | mem = make([]byte, data.Size) 154 | data.Interface = (*wgh.WGInterfaceIO)(unsafe.Pointer(&mem[0])) 155 | } 156 | 157 | return parseDevice(name, data.Interface) 158 | } 159 | 160 | // parseDevice unpacks a Device from ifio, along with its associated peers 161 | // and their allowed IPs. 162 | func parseDevice(name string, ifio *wgh.WGInterfaceIO) (*wgtypes.Device, error) { 163 | d := &wgtypes.Device{ 164 | Name: name, 165 | Type: wgtypes.OpenBSDKernel, 166 | } 167 | 168 | // The kernel populates ifio.Flags to indicate which fields are present. 169 | 170 | if ifio.Flags&wgh.WG_INTERFACE_HAS_PRIVATE != 0 { 171 | d.PrivateKey = wgtypes.Key(ifio.Private) 172 | } 173 | 174 | if ifio.Flags&wgh.WG_INTERFACE_HAS_PUBLIC != 0 { 175 | d.PublicKey = wgtypes.Key(ifio.Public) 176 | } 177 | 178 | if ifio.Flags&wgh.WG_INTERFACE_HAS_PORT != 0 { 179 | d.ListenPort = int(ifio.Port) 180 | } 181 | 182 | if ifio.Flags&wgh.WG_INTERFACE_HAS_RTABLE != 0 { 183 | d.FirewallMark = int(ifio.Rtable) 184 | } 185 | 186 | d.Peers = make([]wgtypes.Peer, 0, ifio.Peers_count) 187 | 188 | // If there were no peers, exit early so we do not advance the pointer 189 | // beyond the end of the WGInterfaceIO structure. 190 | if ifio.Peers_count == 0 { 191 | return d, nil 192 | } 193 | 194 | // Set our pointer to the beginning of the first peer's location in memory. 195 | peer := (*wgh.WGPeerIO)(unsafe.Pointer( 196 | uintptr(unsafe.Pointer(ifio)) + wgh.SizeofWGInterfaceIO, 197 | )) 198 | 199 | for i := 0; i < int(ifio.Peers_count); i++ { 200 | p := parsePeer(peer) 201 | 202 | // Same idea, we know how many allowed IPs we need to account for, so 203 | // reserve the space and advance the pointer through each WGAIP structure. 204 | p.AllowedIPs = make([]net.IPNet, 0, peer.Aips_count) 205 | for j := uintptr(0); j < uintptr(peer.Aips_count); j++ { 206 | aip := (*wgh.WGAIPIO)(unsafe.Pointer( 207 | uintptr(unsafe.Pointer(peer)) + wgh.SizeofWGPeerIO + j*wgh.SizeofWGAIPIO, 208 | )) 209 | 210 | p.AllowedIPs = append(p.AllowedIPs, parseAllowedIP(aip)) 211 | } 212 | 213 | // Prepare for the next iteration. 214 | d.Peers = append(d.Peers, p) 215 | peer = (*wgh.WGPeerIO)(unsafe.Pointer( 216 | uintptr(unsafe.Pointer(peer)) + wgh.SizeofWGPeerIO + 217 | uintptr(peer.Aips_count)*wgh.SizeofWGAIPIO, 218 | )) 219 | } 220 | 221 | return d, nil 222 | } 223 | 224 | // ConfigureDevice implements wginternal.Client. 225 | func (c *Client) ConfigureDevice(name string, cfg wgtypes.Config) error { 226 | // Currently read-only: we must determine if a device belongs to this driver, 227 | // and if it does, return a sentinel so integration tests that configure a 228 | // device can be skipped. 229 | if _, err := c.Device(name); err != nil { 230 | return err 231 | } 232 | 233 | return wginternal.ErrReadOnly 234 | } 235 | 236 | // deviceName converts an interface name string to the format required to pass 237 | // with wgh.WGGetServ. 238 | func deviceName(name string) ([16]byte, error) { 239 | var out [unix.IFNAMSIZ]byte 240 | if len(name) > unix.IFNAMSIZ { 241 | return out, fmt.Errorf("wgopenbsd: interface name %q too long", name) 242 | } 243 | 244 | copy(out[:], name) 245 | return out, nil 246 | } 247 | 248 | // parsePeer unpacks a wgtypes.Peer from a WGPeerIO structure. 249 | func parsePeer(pio *wgh.WGPeerIO) wgtypes.Peer { 250 | p := wgtypes.Peer{ 251 | ReceiveBytes: int64(pio.Rxbytes), 252 | TransmitBytes: int64(pio.Txbytes), 253 | ProtocolVersion: int(pio.Protocol_version), 254 | } 255 | 256 | // Only set last handshake if a non-zero timespec was provided, matching 257 | // the time.Time.IsZero() behavior of internal/wglinux. 258 | if pio.Last_handshake.Sec > 0 && pio.Last_handshake.Nsec > 0 { 259 | p.LastHandshakeTime = time.Unix( 260 | pio.Last_handshake.Sec, 261 | // Conversion required for GOARCH=386. 262 | int64(pio.Last_handshake.Nsec), 263 | ) 264 | } 265 | 266 | if pio.Flags&wgh.WG_PEER_HAS_PUBLIC != 0 { 267 | p.PublicKey = wgtypes.Key(pio.Public) 268 | } 269 | 270 | if pio.Flags&wgh.WG_PEER_HAS_PSK != 0 { 271 | p.PresharedKey = wgtypes.Key(pio.Psk) 272 | } 273 | 274 | if pio.Flags&wgh.WG_PEER_HAS_PKA != 0 { 275 | p.PersistentKeepaliveInterval = time.Duration(pio.Pka) * time.Second 276 | } 277 | 278 | if pio.Flags&wgh.WG_PEER_HAS_ENDPOINT != 0 { 279 | p.Endpoint = parseEndpoint(pio.Endpoint) 280 | } 281 | 282 | return p 283 | } 284 | 285 | // parseAllowedIP unpacks a net.IPNet from a WGAIP structure. 286 | func parseAllowedIP(aip *wgh.WGAIPIO) net.IPNet { 287 | switch aip.Af { 288 | case unix.AF_INET: 289 | return net.IPNet{ 290 | IP: net.IP(aip.Addr[:net.IPv4len]), 291 | Mask: net.CIDRMask(int(aip.Cidr), 32), 292 | } 293 | case unix.AF_INET6: 294 | return net.IPNet{ 295 | IP: net.IP(aip.Addr[:]), 296 | Mask: net.CIDRMask(int(aip.Cidr), 128), 297 | } 298 | default: 299 | panicf("wgopenbsd: invalid address family for allowed IP: %+v", aip) 300 | return net.IPNet{} 301 | } 302 | } 303 | 304 | // parseEndpoint parses a peer endpoint from a wgh.WGIP structure. 305 | func parseEndpoint(ep [28]byte) *net.UDPAddr { 306 | // sockaddr* structures have family at index 1. 307 | switch ep[1] { 308 | case unix.AF_INET: 309 | sa := *(*unix.RawSockaddrInet4)(unsafe.Pointer(&ep[0])) 310 | 311 | ep := &net.UDPAddr{ 312 | IP: make(net.IP, net.IPv4len), 313 | Port: bePort(sa.Port), 314 | } 315 | copy(ep.IP, sa.Addr[:]) 316 | 317 | return ep 318 | case unix.AF_INET6: 319 | sa := *(*unix.RawSockaddrInet6)(unsafe.Pointer(&ep[0])) 320 | 321 | // TODO(mdlayher): IPv6 zone? 322 | ep := &net.UDPAddr{ 323 | IP: make(net.IP, net.IPv6len), 324 | Port: bePort(sa.Port), 325 | } 326 | copy(ep.IP, sa.Addr[:]) 327 | 328 | return ep 329 | default: 330 | // No endpoint configured. 331 | return nil 332 | } 333 | } 334 | 335 | // bePort interprets a port integer stored in native endianness as a big 336 | // endian value. This is necessary for proper endpoint port handling on 337 | // little endian machines. 338 | func bePort(port uint16) int { 339 | b := *(*[2]byte)(unsafe.Pointer(&port)) 340 | return int(binary.BigEndian.Uint16(b[:])) 341 | } 342 | 343 | // ioctlIfgroupreq returns a function which performs the appropriate ioctl on 344 | // fd to retrieve members of an interface group. 345 | func ioctlIfgroupreq(fd int) func(*wgh.Ifgroupreq) error { 346 | return func(ifg *wgh.Ifgroupreq) error { 347 | return ioctl(fd, unix.SIOCGIFGMEMB, unsafe.Pointer(ifg)) 348 | } 349 | } 350 | 351 | // ioctlWGDataIO returns a function which performs the appropriate ioctl on 352 | // fd to issue a WireGuard data I/O. 353 | func ioctlWGDataIO(fd int) func(*wgh.WGDataIO) error { 354 | return func(data *wgh.WGDataIO) error { 355 | return ioctl(fd, wgh.SIOCGWG, unsafe.Pointer(data)) 356 | } 357 | } 358 | 359 | // ioctl is a raw wrapper for the ioctl system call. 360 | func ioctl(fd int, req uint, arg unsafe.Pointer) error { 361 | //lint:ignore SA1019 temporarily permitted until we switch to a libc wrapper 362 | _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(arg)) 363 | if errno != 0 { 364 | return os.NewSyscallError("ioctl", errno) 365 | } 366 | 367 | return nil 368 | } 369 | 370 | func panicf(format string, a ...interface{}) { 371 | panic(fmt.Sprintf(format, a...)) 372 | } 373 | -------------------------------------------------------------------------------- /internal/wgfreebsd/client_freebsd.go: -------------------------------------------------------------------------------- 1 | //go:build freebsd 2 | // +build freebsd 3 | 4 | package wgfreebsd 5 | 6 | // #include 7 | // #include 8 | import "C" 9 | 10 | import ( 11 | "bytes" 12 | "encoding/binary" 13 | "fmt" 14 | "net" 15 | "os" 16 | "runtime" 17 | "time" 18 | "unsafe" 19 | 20 | "golang.org/x/sys/unix" 21 | "golang.zx2c4.com/wireguard/wgctrl/internal/wgfreebsd/internal/nv" 22 | "golang.zx2c4.com/wireguard/wgctrl/internal/wgfreebsd/internal/wgh" 23 | "golang.zx2c4.com/wireguard/wgctrl/internal/wginternal" 24 | "golang.zx2c4.com/wireguard/wgctrl/wgtypes" 25 | ) 26 | 27 | // ifGroupWG is the WireGuard interface group name passed to the kernel. 28 | var ifGroupWG = [16]byte{0: 'w', 1: 'g'} 29 | 30 | var _ wginternal.Client = &Client{} 31 | 32 | // A Client provides access to FreeBSD WireGuard ioctl information. 33 | type Client struct { 34 | // Hooks which use system calls by default, but can also be swapped out 35 | // during tests. 36 | close func() error 37 | ioctlIfgroupreq func(*wgh.Ifgroupreq) error 38 | ioctlWGDataIO func(uint, *wgh.WGDataIO) error 39 | } 40 | 41 | // New creates a new Client and returns whether or not the ioctl interface 42 | // is available. 43 | func New() (*Client, bool, error) { 44 | // The FreeBSD ioctl interface operates on a generic AF_INET socket. 45 | fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0) 46 | if err != nil { 47 | return nil, false, err 48 | } 49 | 50 | // TODO(mdlayher): find a call to invoke here to probe for availability. 51 | // c.Devices won't work because it returns a "not found" error when the 52 | // kernel WireGuard implementation is available but the interface group 53 | // has no members. 54 | 55 | // By default, use system call implementations for all hook functions. 56 | return &Client{ 57 | close: func() error { return unix.Close(fd) }, 58 | ioctlIfgroupreq: ioctlIfgroupreq(fd), 59 | ioctlWGDataIO: ioctlWGDataIO(fd), 60 | }, true, nil 61 | } 62 | 63 | // Close implements wginternal.Client. 64 | func (c *Client) Close() error { 65 | return c.close() 66 | } 67 | 68 | // Devices implements wginternal.Client. 69 | func (c *Client) Devices() ([]*wgtypes.Device, error) { 70 | ifg := wgh.Ifgroupreq{ 71 | // Query for devices in the "wg" group. 72 | Name: ifGroupWG, 73 | } 74 | 75 | // Determine how many device names we must allocate memory for. 76 | if err := c.ioctlIfgroupreq(&ifg); err != nil { 77 | return nil, err 78 | } 79 | 80 | // ifg.Len is size in bytes; allocate enough memory for the correct number 81 | // of wgh.Ifgreq and then store a pointer to the memory where the data 82 | // should be written (ifgrs) in ifg.Groups. 83 | // 84 | // From a thread in golang-nuts, this pattern is valid: 85 | // "It would be OK to pass a pointer to a struct to ioctl if the struct 86 | // contains a pointer to other Go memory, but the struct field must have 87 | // pointer type." 88 | // See: https://groups.google.com/forum/#!topic/golang-nuts/FfasFTZvU_o. 89 | ifgrs := make([]wgh.Ifgreq, ifg.Len/wgh.SizeofIfgreq) 90 | ifg.Groups = &ifgrs[0] 91 | 92 | // Now actually fetch the device names. 93 | if err := c.ioctlIfgroupreq(&ifg); err != nil { 94 | return nil, err 95 | } 96 | 97 | // Keep this alive until we're done doing the ioctl dance. 98 | runtime.KeepAlive(&ifg) 99 | 100 | devices := make([]*wgtypes.Device, 0, len(ifgrs)) 101 | for _, ifgr := range ifgrs { 102 | // Remove any trailing NULL bytes from the interface names. 103 | name := string(bytes.TrimRight(ifgr.Ifgrqu[:], "\x00")) 104 | 105 | device, err := c.Device(name) 106 | if err != nil { 107 | return nil, err 108 | } 109 | 110 | devices = append(devices, device) 111 | } 112 | 113 | return devices, nil 114 | } 115 | 116 | // Device implements wginternal.Client. 117 | func (c *Client) Device(name string) (*wgtypes.Device, error) { 118 | dname, err := deviceName(name) 119 | if err != nil { 120 | return nil, err 121 | } 122 | 123 | // First, specify the name of the device and determine how much memory 124 | // must be allocated. 125 | data := wgh.WGDataIO{ 126 | Name: dname, 127 | } 128 | 129 | var mem []byte 130 | for { 131 | if err := c.ioctlWGDataIO(wgh.SIOCGWG, &data); err != nil { 132 | // ioctl functions always return a wrapped unix.Errno value. 133 | // Conform to the wgctrl contract by unwrapping some values: 134 | // ENXIO: "no such device": (no such WireGuard device) 135 | // EINVAL: "inappropriate ioctl for device" (device is not a 136 | // WireGuard device) 137 | switch err.(*os.SyscallError).Err { 138 | case unix.ENXIO, unix.EINVAL: 139 | return nil, os.ErrNotExist 140 | default: 141 | return nil, err 142 | } 143 | } 144 | 145 | if len(mem) >= int(data.Size) { 146 | // Allocated enough memory! 147 | break 148 | } 149 | 150 | // Allocate the appropriate amount of memory and point the kernel at 151 | // the first byte of our slice's backing array. When the loop continues, 152 | // we will check if we've allocated enough memory. 153 | mem = make([]byte, data.Size) 154 | data.Data = &mem[0] 155 | } 156 | 157 | dev, err := parseDevice(mem) 158 | if err != nil { 159 | return nil, err 160 | } 161 | 162 | dev.Name = name 163 | 164 | return dev, nil 165 | } 166 | 167 | // ConfigureDevice implements wginternal.Client. 168 | func (c *Client) ConfigureDevice(name string, cfg wgtypes.Config) error { 169 | // Check if there is a peer with the UpdateOnly flag set. 170 | // This is not supported on FreeBSD yet. So error out.. 171 | // TODO(stv0g): remove this check once kernel support has landed. 172 | for _, peer := range cfg.Peers { 173 | if peer.UpdateOnly { 174 | // Check that this device is really an existing kernel 175 | // device 176 | if _, err := c.Device(name); err != os.ErrNotExist { 177 | return wgtypes.ErrUpdateOnlyNotSupported 178 | } 179 | } 180 | } 181 | 182 | m := unparseConfig(cfg) 183 | mem, sz, err := nv.Marshal(m) 184 | if err != nil { 185 | return err 186 | } 187 | defer C.free(unsafe.Pointer(mem)) 188 | 189 | dname, err := deviceName(name) 190 | if err != nil { 191 | return err 192 | } 193 | 194 | data := wgh.WGDataIO{ 195 | Name: dname, 196 | Data: mem, 197 | Size: uint64(sz), 198 | } 199 | 200 | if err := c.ioctlWGDataIO(wgh.SIOCSWG, &data); err != nil { 201 | // ioctl functions always return a wrapped unix.Errno value. 202 | // Conform to the wgctrl contract by unwrapping some values: 203 | // ENXIO: "no such device": (no such WireGuard device) 204 | // EINVAL: "inappropriate ioctl for device" (device is not a 205 | // WireGuard device) 206 | switch err.(*os.SyscallError).Err { 207 | case unix.ENXIO, unix.EINVAL: 208 | return os.ErrNotExist 209 | default: 210 | return err 211 | } 212 | } 213 | 214 | return nil 215 | } 216 | 217 | // deviceName converts an interface name string to the format required to pass 218 | // with wgh.WGGetServ. 219 | func deviceName(name string) ([16]byte, error) { 220 | var out [unix.IFNAMSIZ]byte 221 | if len(name) > unix.IFNAMSIZ { 222 | return out, fmt.Errorf("wgfreebsd: interface name %q too long", name) 223 | } 224 | 225 | copy(out[:], name) 226 | return out, nil 227 | } 228 | 229 | // ioctlIfgroupreq returns a function which performs the appropriate ioctl on 230 | // fd to retrieve members of an interface group. 231 | func ioctlIfgroupreq(fd int) func(*wgh.Ifgroupreq) error { 232 | return func(ifg *wgh.Ifgroupreq) error { 233 | return ioctl(fd, unix.SIOCGIFGMEMB, unsafe.Pointer(ifg)) 234 | } 235 | } 236 | 237 | // ioctlWGDataIO returns a function which performs the appropriate ioctl on 238 | // fd to issue a WireGuard data I/O. 239 | func ioctlWGDataIO(fd int) func(uint, *wgh.WGDataIO) error { 240 | return func(req uint, data *wgh.WGDataIO) error { 241 | return ioctl(fd, req, unsafe.Pointer(data)) 242 | } 243 | } 244 | 245 | // ioctl is a raw wrapper for the ioctl system call. 246 | func ioctl(fd int, req uint, arg unsafe.Pointer) error { 247 | _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(arg)) 248 | if errno != 0 { 249 | return os.NewSyscallError("ioctl", errno) 250 | } 251 | 252 | return nil 253 | } 254 | 255 | func panicf(format string, a ...interface{}) { 256 | panic(fmt.Sprintf(format, a...)) 257 | } 258 | 259 | func ntohs(i uint16) int { 260 | b := *(*[2]byte)(unsafe.Pointer(&i)) 261 | return int(binary.BigEndian.Uint16(b[:])) 262 | } 263 | 264 | func htons(i int) uint16 { 265 | b := make([]byte, 2) 266 | binary.BigEndian.PutUint16(b, uint16(i)) 267 | return *(*uint16)(unsafe.Pointer(&b[0])) 268 | } 269 | 270 | // parseEndpoint converts a struct sockaddr to a Go net.UDPAddr 271 | func parseEndpoint(ep []byte) *net.UDPAddr { 272 | sa := (*unix.RawSockaddr)(unsafe.Pointer(&ep[0])) 273 | 274 | switch sa.Family { 275 | case unix.AF_INET: 276 | sa := (*unix.RawSockaddrInet4)(unsafe.Pointer(&ep[0])) 277 | 278 | ep := &net.UDPAddr{ 279 | IP: make(net.IP, net.IPv4len), 280 | Port: ntohs(sa.Port), 281 | } 282 | copy(ep.IP, sa.Addr[:]) 283 | 284 | return ep 285 | case unix.AF_INET6: 286 | sa := (*unix.RawSockaddrInet6)(unsafe.Pointer(&ep[0])) 287 | 288 | // TODO(mdlayher): IPv6 zone? 289 | ep := &net.UDPAddr{ 290 | IP: make(net.IP, net.IPv6len), 291 | Port: ntohs(sa.Port), 292 | } 293 | copy(ep.IP, sa.Addr[:]) 294 | 295 | return ep 296 | default: 297 | // No endpoint configured. 298 | return nil 299 | } 300 | } 301 | 302 | func unparseEndpoint(ep net.UDPAddr) []byte { 303 | var b []byte 304 | 305 | if v4 := ep.IP.To4(); v4 != nil { 306 | b = make([]byte, unsafe.Sizeof(unix.RawSockaddrInet4{})) 307 | sa := (*unix.RawSockaddrInet4)(unsafe.Pointer(&b[0])) 308 | 309 | sa.Family = unix.AF_INET 310 | sa.Port = htons(ep.Port) 311 | copy(sa.Addr[:], v4) 312 | } else if v6 := ep.IP.To16(); v6 != nil { 313 | b = make([]byte, unsafe.Sizeof(unix.RawSockaddrInet6{})) 314 | sa := (*unix.RawSockaddrInet6)(unsafe.Pointer(&b[0])) 315 | 316 | sa.Family = unix.AF_INET6 317 | sa.Port = htons(ep.Port) 318 | copy(sa.Addr[:], v6) 319 | } 320 | 321 | return b 322 | } 323 | 324 | // parseAllowedIP unpacks a net.IPNet from a WGAIP structure. 325 | func parseAllowedIP(aip nv.List) net.IPNet { 326 | cidr := int(aip["cidr"].(uint64)) 327 | if ip, ok := aip["ipv4"]; ok { 328 | return net.IPNet{ 329 | IP: net.IP(ip.([]byte)), 330 | Mask: net.CIDRMask(cidr, 32), 331 | } 332 | } else if ip, ok := aip["ipv6"]; ok { 333 | return net.IPNet{ 334 | IP: net.IP(ip.([]byte)), 335 | Mask: net.CIDRMask(cidr, 128), 336 | } 337 | } else { 338 | panicf("wgfreebsd: invalid address family for allowed IP: %+v", aip) 339 | return net.IPNet{} 340 | } 341 | } 342 | 343 | func unparseAllowedIP(aip net.IPNet) nv.List { 344 | m := nv.List{} 345 | 346 | ones, _ := aip.Mask.Size() 347 | m["cidr"] = uint64(ones) 348 | 349 | if v4 := aip.IP.To4(); v4 != nil { 350 | m["ipv4"] = []byte(v4) 351 | } else if v6 := aip.IP.To16(); v6 != nil { 352 | m["ipv6"] = []byte(v6) 353 | } 354 | 355 | return m 356 | } 357 | 358 | // parseTimestamp parses a binary timestamp to a Go time.Time 359 | func parseTimestamp(b []byte) time.Time { 360 | var secs, nsecs int64 361 | 362 | buf := bytes.NewReader(b) 363 | 364 | // TODO(stv0g): Handle non-little endian machines 365 | binary.Read(buf, binary.LittleEndian, &secs) 366 | binary.Read(buf, binary.LittleEndian, &nsecs) 367 | 368 | if secs == 0 && nsecs == 0 { 369 | return time.Time{} 370 | } 371 | 372 | return time.Unix(secs, nsecs) 373 | } 374 | 375 | // parsePeer unpacks a wgtypes.Peer from a name-value list (nvlist). 376 | func parsePeer(v nv.List) wgtypes.Peer { 377 | p := wgtypes.Peer{ 378 | ProtocolVersion: 1, 379 | } 380 | 381 | if v, ok := v["public-key"]; ok { 382 | pk := (*wgtypes.Key)(v.([]byte)) 383 | p.PublicKey = *pk 384 | } 385 | 386 | if v, ok := v["preshared-key"]; ok { 387 | psk := (*wgtypes.Key)(v.([]byte)) 388 | p.PresharedKey = *psk 389 | } 390 | 391 | if v, ok := v["last-handshake-time"]; ok { 392 | p.LastHandshakeTime = parseTimestamp(v.([]byte)) 393 | } 394 | 395 | if v, ok := v["endpoint"]; ok { 396 | p.Endpoint = parseEndpoint(v.([]byte)) 397 | } 398 | 399 | if v, ok := v["persistent-keepalive-interval"]; ok { 400 | p.PersistentKeepaliveInterval = time.Second * time.Duration(v.(uint64)) 401 | } 402 | 403 | if v, ok := v["rx-bytes"]; ok { 404 | p.ReceiveBytes = int64(v.(uint64)) 405 | } 406 | 407 | if v, ok := v["tx-bytes"]; ok { 408 | p.TransmitBytes = int64(v.(uint64)) 409 | } 410 | 411 | if v, ok := v["allowed-ips"]; ok { 412 | m := v.([]nv.List) 413 | for _, aip := range m { 414 | p.AllowedIPs = append(p.AllowedIPs, parseAllowedIP(aip)) 415 | } 416 | } 417 | 418 | return p 419 | } 420 | 421 | // parseDevice decodes the device from a FreeBSD name-value list (nvlist) 422 | func parseDevice(data []byte) (*wgtypes.Device, error) { 423 | dev := &wgtypes.Device{ 424 | Type: wgtypes.FreeBSDKernel, 425 | } 426 | 427 | m := nv.List{} 428 | if err := nv.Unmarshal(data, m); err != nil { 429 | return nil, err 430 | } 431 | 432 | if v, ok := m["public-key"]; ok { 433 | pk := (*wgtypes.Key)(v.([]byte)) 434 | dev.PublicKey = *pk 435 | } 436 | 437 | if v, ok := m["private-key"]; ok { 438 | sk := (*wgtypes.Key)(v.([]byte)) 439 | dev.PrivateKey = *sk 440 | } 441 | 442 | if v, ok := m["user-cookie"]; ok { 443 | dev.FirewallMark = int(v.(uint64)) 444 | } 445 | 446 | if v, ok := m["listen-port"]; ok { 447 | dev.ListenPort = int(v.(uint64)) 448 | } 449 | 450 | if v, ok := m["peers"]; ok { 451 | m := v.([]nv.List) 452 | for _, n := range m { 453 | peer := parsePeer(n) 454 | dev.Peers = append(dev.Peers, peer) 455 | } 456 | } 457 | 458 | return dev, nil 459 | } 460 | 461 | // unparsePeerConfig encodes a PeerConfig to a name-value list (nvlist). 462 | func unparsePeerConfig(cfg wgtypes.PeerConfig) nv.List { 463 | m := nv.List{} 464 | 465 | m["public-key"] = cfg.PublicKey[:] 466 | 467 | if v := cfg.PresharedKey; v != nil { 468 | m["preshared-key"] = v[:] 469 | } 470 | 471 | if v := cfg.PersistentKeepaliveInterval; v != nil { 472 | m["persistent-keepalive-interval"] = uint64(v.Seconds()) 473 | } 474 | 475 | if v := cfg.Endpoint; v != nil { 476 | m["endpoint"] = unparseEndpoint(*v) 477 | } 478 | 479 | if cfg.ReplaceAllowedIPs { 480 | m["replace-allowedips"] = true 481 | } 482 | 483 | if cfg.Remove { 484 | m["remove"] = true 485 | } 486 | 487 | if cfg.AllowedIPs != nil { 488 | aips := []nv.List{} 489 | 490 | for _, aip := range cfg.AllowedIPs { 491 | aips = append(aips, unparseAllowedIP(aip)) 492 | } 493 | 494 | m["allowed-ips"] = aips 495 | } 496 | 497 | return m 498 | } 499 | 500 | // unparseDevice encodes the device configuration as a FreeBSD name-value list (nvlist). 501 | func unparseConfig(cfg wgtypes.Config) nv.List { 502 | m := nv.List{} 503 | 504 | if v := cfg.PrivateKey; v != nil { 505 | m["private-key"] = v[:] 506 | } 507 | 508 | if v := cfg.ListenPort; v != nil { 509 | m["listen-port"] = uint64(*v) 510 | } 511 | 512 | if v := cfg.FirewallMark; v != nil { 513 | m["user-cookie"] = uint64(*v) 514 | } 515 | 516 | if cfg.ReplacePeers { 517 | m["replace-peers"] = true 518 | } 519 | 520 | if v := cfg.Peers; v != nil { 521 | peers := []nv.List{} 522 | 523 | for _, p := range v { 524 | peer := unparsePeerConfig(p) 525 | peers = append(peers, peer) 526 | } 527 | 528 | m["peers"] = peers 529 | } 530 | 531 | return m 532 | } 533 | -------------------------------------------------------------------------------- /internal/wglinux/configure_linux_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package wglinux 5 | 6 | import ( 7 | "net" 8 | "testing" 9 | "time" 10 | "unsafe" 11 | 12 | "github.com/mdlayher/genetlink" 13 | "github.com/mdlayher/genetlink/genltest" 14 | "github.com/mdlayher/netlink" 15 | "github.com/mdlayher/netlink/nlenc" 16 | "github.com/mikioh/ipaddr" 17 | "golang.org/x/sys/unix" 18 | "golang.zx2c4.com/wireguard/wgctrl/internal/wgtest" 19 | "golang.zx2c4.com/wireguard/wgctrl/wgtypes" 20 | ) 21 | 22 | func TestLinuxClientConfigureDevice(t *testing.T) { 23 | nameAttr := netlink.Attribute{ 24 | Type: unix.WGDEVICE_A_IFNAME, 25 | Data: nlenc.Bytes(okName), 26 | } 27 | 28 | tests := []struct { 29 | name string 30 | cfg wgtypes.Config 31 | attrs []netlink.Attribute 32 | ok bool 33 | }{ 34 | { 35 | name: "bad peer endpoint", 36 | cfg: wgtypes.Config{ 37 | Peers: []wgtypes.PeerConfig{{ 38 | Endpoint: &net.UDPAddr{ 39 | IP: net.IP{0xff}, 40 | }, 41 | }}, 42 | }, 43 | }, 44 | { 45 | name: "bad peer allowed IP", 46 | cfg: wgtypes.Config{ 47 | Peers: []wgtypes.PeerConfig{{ 48 | AllowedIPs: []net.IPNet{{ 49 | IP: net.IP{0xff}, 50 | }}, 51 | }}, 52 | }, 53 | }, 54 | { 55 | name: "ok, none", 56 | attrs: []netlink.Attribute{ 57 | nameAttr, 58 | }, 59 | ok: true, 60 | }, 61 | { 62 | name: "ok, all", 63 | cfg: wgtypes.Config{ 64 | PrivateKey: keyPtr(wgtest.MustHexKey("e84b5a6d2717c1003a13b431570353dbaca9146cf150c5f8575680feba52027a")), 65 | ListenPort: intPtr(12912), 66 | FirewallMark: intPtr(0), 67 | ReplacePeers: true, 68 | Peers: []wgtypes.PeerConfig{ 69 | { 70 | PublicKey: wgtest.MustHexKey("b85996fecc9c7f1fc6d2572a76eda11d59bcd20be8e543b15ce4bd85a8e75a33"), 71 | PresharedKey: keyPtr(wgtest.MustHexKey("188515093e952f5f22e865cef3012e72f8b5f0b598ac0309d5dacce3b70fcf52")), 72 | Endpoint: wgtest.MustUDPAddr("[abcd:23::33%2]:51820"), 73 | ReplaceAllowedIPs: true, 74 | AllowedIPs: []net.IPNet{ 75 | wgtest.MustCIDR("192.168.4.4/32"), 76 | }, 77 | }, 78 | { 79 | PublicKey: wgtest.MustHexKey("58402e695ba1772b1cc9309755f043251ea77fdcf10fbe63989ceb7e19321376"), 80 | UpdateOnly: true, 81 | Endpoint: wgtest.MustUDPAddr("182.122.22.19:3233"), 82 | PersistentKeepaliveInterval: durPtr(111 * time.Second), 83 | ReplaceAllowedIPs: true, 84 | AllowedIPs: []net.IPNet{ 85 | wgtest.MustCIDR("192.168.4.6/32"), 86 | }, 87 | }, 88 | { 89 | PublicKey: wgtest.MustHexKey("662e14fd594556f522604703340351258903b64f35553763f19426ab2a515c58"), 90 | Endpoint: wgtest.MustUDPAddr("5.152.198.39:51820"), 91 | ReplaceAllowedIPs: true, 92 | AllowedIPs: []net.IPNet{ 93 | wgtest.MustCIDR("192.168.4.10/32"), 94 | wgtest.MustCIDR("192.168.4.11/32"), 95 | }, 96 | }, 97 | { 98 | PublicKey: wgtest.MustHexKey("e818b58db5274087fcc1be5dc728cf53d3b5726b4cef6b9bab8f8f8c2452c25c"), 99 | Remove: true, 100 | }, 101 | }, 102 | }, 103 | attrs: []netlink.Attribute{ 104 | nameAttr, 105 | { 106 | Type: unix.WGDEVICE_A_PRIVATE_KEY, 107 | Data: keyBytes("e84b5a6d2717c1003a13b431570353dbaca9146cf150c5f8575680feba52027a"), 108 | }, 109 | { 110 | Type: unix.WGDEVICE_A_LISTEN_PORT, 111 | Data: nlenc.Uint16Bytes(12912), 112 | }, 113 | { 114 | Type: unix.WGDEVICE_A_FWMARK, 115 | Data: nlenc.Uint32Bytes(0), 116 | }, 117 | { 118 | Type: unix.WGDEVICE_A_FLAGS, 119 | Data: nlenc.Uint32Bytes(unix.WGDEVICE_F_REPLACE_PEERS), 120 | }, 121 | { 122 | Type: netlink.Nested | unix.WGDEVICE_A_PEERS, 123 | Data: m([]netlink.Attribute{ 124 | { 125 | Type: netlink.Nested, 126 | Data: m([]netlink.Attribute{ 127 | { 128 | Type: unix.WGPEER_A_PUBLIC_KEY, 129 | Data: keyBytes("b85996fecc9c7f1fc6d2572a76eda11d59bcd20be8e543b15ce4bd85a8e75a33"), 130 | }, 131 | { 132 | Type: unix.WGPEER_A_FLAGS, 133 | Data: nlenc.Uint32Bytes(unix.WGPEER_F_REPLACE_ALLOWEDIPS), 134 | }, 135 | { 136 | Type: unix.WGPEER_A_PRESHARED_KEY, 137 | Data: keyBytes("188515093e952f5f22e865cef3012e72f8b5f0b598ac0309d5dacce3b70fcf52"), 138 | }, 139 | { 140 | Type: unix.WGPEER_A_ENDPOINT, 141 | Data: (*(*[unix.SizeofSockaddrInet6]byte)(unsafe.Pointer(&unix.RawSockaddrInet6{ 142 | Family: unix.AF_INET6, 143 | Addr: [16]byte{ 144 | 0xab, 0xcd, 0x00, 0x23, 145 | 0x00, 0x00, 0x00, 0x00, 146 | 0x00, 0x00, 0x00, 0x00, 147 | 0x00, 0x00, 0x00, 0x33, 148 | }, 149 | Port: sockaddrPort(51820), 150 | })))[:], 151 | }, 152 | { 153 | Type: netlink.Nested | unix.WGPEER_A_ALLOWEDIPS, 154 | Data: mustAllowedIPs([]net.IPNet{ 155 | wgtest.MustCIDR("192.168.4.4/32"), 156 | }), 157 | }, 158 | }...), 159 | }, 160 | { 161 | Type: netlink.Nested | 1, 162 | Data: m([]netlink.Attribute{ 163 | { 164 | Type: unix.WGPEER_A_PUBLIC_KEY, 165 | Data: keyBytes("58402e695ba1772b1cc9309755f043251ea77fdcf10fbe63989ceb7e19321376"), 166 | }, 167 | { 168 | Type: unix.WGPEER_A_FLAGS, 169 | Data: nlenc.Uint32Bytes(unix.WGPEER_F_REPLACE_ALLOWEDIPS | unix.WGPEER_F_UPDATE_ONLY), 170 | }, 171 | { 172 | Type: unix.WGPEER_A_ENDPOINT, 173 | Data: (*(*[unix.SizeofSockaddrInet4]byte)(unsafe.Pointer(&unix.RawSockaddrInet4{ 174 | Family: unix.AF_INET, 175 | Addr: [4]byte{182, 122, 22, 19}, 176 | Port: sockaddrPort(3233), 177 | })))[:], 178 | }, 179 | { 180 | Type: unix.WGPEER_A_PERSISTENT_KEEPALIVE_INTERVAL, 181 | Data: nlenc.Uint16Bytes(111), 182 | }, 183 | { 184 | Type: netlink.Nested | unix.WGPEER_A_ALLOWEDIPS, 185 | Data: mustAllowedIPs([]net.IPNet{ 186 | wgtest.MustCIDR("192.168.4.6/32"), 187 | }), 188 | }, 189 | }...), 190 | }, 191 | { 192 | Type: netlink.Nested | 2, 193 | Data: m([]netlink.Attribute{ 194 | { 195 | Type: unix.WGPEER_A_PUBLIC_KEY, 196 | Data: keyBytes("662e14fd594556f522604703340351258903b64f35553763f19426ab2a515c58"), 197 | }, 198 | { 199 | Type: unix.WGPEER_A_FLAGS, 200 | Data: nlenc.Uint32Bytes(unix.WGPEER_F_REPLACE_ALLOWEDIPS), 201 | }, 202 | { 203 | Type: unix.WGPEER_A_ENDPOINT, 204 | Data: (*(*[unix.SizeofSockaddrInet4]byte)(unsafe.Pointer(&unix.RawSockaddrInet4{ 205 | Family: unix.AF_INET, 206 | Addr: [4]byte{5, 152, 198, 39}, 207 | Port: sockaddrPort(51820), 208 | })))[:], 209 | }, 210 | { 211 | Type: netlink.Nested | unix.WGPEER_A_ALLOWEDIPS, 212 | Data: mustAllowedIPs([]net.IPNet{ 213 | wgtest.MustCIDR("192.168.4.10/32"), 214 | wgtest.MustCIDR("192.168.4.11/32"), 215 | }), 216 | }, 217 | }...), 218 | }, 219 | { 220 | Type: netlink.Nested | 3, 221 | Data: m([]netlink.Attribute{ 222 | { 223 | Type: unix.WGPEER_A_PUBLIC_KEY, 224 | Data: keyBytes("e818b58db5274087fcc1be5dc728cf53d3b5726b4cef6b9bab8f8f8c2452c25c"), 225 | }, 226 | { 227 | Type: unix.WGPEER_A_FLAGS, 228 | Data: nlenc.Uint32Bytes(unix.WGPEER_F_REMOVE_ME), 229 | }, 230 | }...), 231 | }, 232 | }...), 233 | }, 234 | }, 235 | ok: true, 236 | }, 237 | } 238 | 239 | for _, tt := range tests { 240 | t.Run(tt.name, func(t *testing.T) { 241 | const ( 242 | cmd = unix.WG_CMD_SET_DEVICE 243 | flags = netlink.Request | netlink.Acknowledge 244 | ) 245 | 246 | fn := func(greq genetlink.Message, _ netlink.Message) ([]genetlink.Message, error) { 247 | attrs, err := netlink.UnmarshalAttributes(greq.Data) 248 | if err != nil { 249 | return nil, err 250 | } 251 | 252 | if diff := diffAttrs(tt.attrs, attrs); diff != "" { 253 | t.Fatalf("unexpected request attributes (-want +got):\n%s", diff) 254 | } 255 | 256 | // Data currently unused; send a message to acknowledge request. 257 | return []genetlink.Message{{}}, nil 258 | } 259 | 260 | c := testClient(t, genltest.CheckRequest(familyID, cmd, flags, fn)) 261 | defer c.Close() 262 | 263 | err := c.ConfigureDevice(okName, tt.cfg) 264 | 265 | if tt.ok && err != nil { 266 | t.Fatalf("failed to configure device: %v", err) 267 | } 268 | if !tt.ok && err == nil { 269 | t.Fatal("expected an error, but none occurred") 270 | } 271 | }) 272 | } 273 | } 274 | 275 | func TestLinuxClientConfigureDeviceLargePeerIPChunks(t *testing.T) { 276 | nameAttr := netlink.Attribute{ 277 | Type: unix.WGDEVICE_A_IFNAME, 278 | Data: nlenc.Bytes(okName), 279 | } 280 | 281 | var ( 282 | peerA = wgtest.MustPublicKey() 283 | peerAIPs = generateIPs(ipBatchChunk + 1) 284 | 285 | peerB = wgtest.MustPublicKey() 286 | peerBIPs = generateIPs(ipBatchChunk / 2) 287 | 288 | peerC = wgtest.MustPublicKey() 289 | peerCIPs = generateIPs(ipBatchChunk * 3) 290 | 291 | peerD = wgtest.MustPublicKey() 292 | ) 293 | 294 | cfg := wgtypes.Config{ 295 | ReplacePeers: true, 296 | Peers: []wgtypes.PeerConfig{ 297 | { 298 | PublicKey: peerA, 299 | UpdateOnly: true, 300 | ReplaceAllowedIPs: true, 301 | 302 | AllowedIPs: peerAIPs, 303 | }, 304 | { 305 | PublicKey: peerB, 306 | UpdateOnly: true, 307 | ReplaceAllowedIPs: true, 308 | AllowedIPs: peerBIPs, 309 | }, 310 | { 311 | PublicKey: peerC, 312 | UpdateOnly: true, 313 | ReplaceAllowedIPs: true, 314 | AllowedIPs: peerCIPs, 315 | }, 316 | { 317 | PublicKey: peerD, 318 | Remove: true, 319 | }, 320 | }, 321 | } 322 | 323 | var allAttrs []netlink.Attribute 324 | fn := func(greq genetlink.Message, _ netlink.Message) ([]genetlink.Message, error) { 325 | attrs, err := netlink.UnmarshalAttributes(greq.Data) 326 | if err != nil { 327 | return nil, err 328 | } 329 | 330 | allAttrs = append(allAttrs, attrs...) 331 | 332 | // Data currently unused; send a message to acknowledge request. 333 | return []genetlink.Message{{}}, nil 334 | } 335 | 336 | c := testClient(t, fn) 337 | defer c.Close() 338 | 339 | if err := c.ConfigureDevice(okName, cfg); err != nil { 340 | t.Fatalf("failed to configure: %v", err) 341 | } 342 | 343 | want := []netlink.Attribute{ 344 | // First peer, first chunk. 345 | nameAttr, 346 | { 347 | Type: unix.WGDEVICE_A_FLAGS, 348 | Data: nlenc.Uint32Bytes(unix.WGDEVICE_F_REPLACE_PEERS), 349 | }, 350 | { 351 | Type: netlink.Nested | unix.WGDEVICE_A_PEERS, 352 | Data: m(netlink.Attribute{ 353 | Type: netlink.Nested, 354 | Data: m([]netlink.Attribute{ 355 | { 356 | Type: unix.WGPEER_A_PUBLIC_KEY, 357 | Data: peerA[:], 358 | }, 359 | { 360 | Type: unix.WGPEER_A_FLAGS, 361 | Data: nlenc.Uint32Bytes(unix.WGPEER_F_REPLACE_ALLOWEDIPS | unix.WGPEER_F_UPDATE_ONLY), 362 | }, 363 | { 364 | Type: netlink.Nested | unix.WGPEER_A_ALLOWEDIPS, 365 | Data: mustAllowedIPs(peerAIPs[:ipBatchChunk]), 366 | }, 367 | }...), 368 | }), 369 | }, 370 | // First peer, final chunk. 371 | nameAttr, 372 | { 373 | Type: netlink.Nested | unix.WGDEVICE_A_PEERS, 374 | Data: m(netlink.Attribute{ 375 | Type: netlink.Nested, 376 | Data: m([]netlink.Attribute{ 377 | { 378 | Type: unix.WGPEER_A_PUBLIC_KEY, 379 | Data: peerA[:], 380 | }, 381 | { 382 | Type: unix.WGPEER_A_FLAGS, 383 | Data: nlenc.Uint32Bytes(unix.WGPEER_F_UPDATE_ONLY), 384 | }, 385 | // Not first chunk; don't replace IPs. 386 | { 387 | Type: netlink.Nested | unix.WGPEER_A_ALLOWEDIPS, 388 | Data: mustAllowedIPs(peerAIPs[ipBatchChunk:]), 389 | }, 390 | }...), 391 | }), 392 | }, 393 | // Second peer, only chunk. 394 | nameAttr, 395 | // This is not the first peer; don't replace existing peers. 396 | { 397 | Type: netlink.Nested | unix.WGDEVICE_A_PEERS, 398 | Data: m(netlink.Attribute{ 399 | Type: netlink.Nested, 400 | Data: m([]netlink.Attribute{ 401 | { 402 | Type: unix.WGPEER_A_PUBLIC_KEY, 403 | Data: peerB[:], 404 | }, 405 | { 406 | Type: unix.WGPEER_A_FLAGS, 407 | Data: nlenc.Uint32Bytes(unix.WGPEER_F_REPLACE_ALLOWEDIPS | unix.WGPEER_F_UPDATE_ONLY), 408 | }, 409 | { 410 | Type: netlink.Nested | unix.WGPEER_A_ALLOWEDIPS, 411 | Data: mustAllowedIPs(peerBIPs), 412 | }, 413 | }...), 414 | }), 415 | }, 416 | // Third peer, first chunk. 417 | nameAttr, 418 | // This is not the first peer; don't replace existing peers. 419 | { 420 | Type: netlink.Nested | unix.WGDEVICE_A_PEERS, 421 | Data: m(netlink.Attribute{ 422 | Type: netlink.Nested, 423 | Data: m([]netlink.Attribute{ 424 | { 425 | Type: unix.WGPEER_A_PUBLIC_KEY, 426 | Data: peerC[:], 427 | }, 428 | { 429 | Type: unix.WGPEER_A_FLAGS, 430 | Data: nlenc.Uint32Bytes(unix.WGPEER_F_REPLACE_ALLOWEDIPS | unix.WGPEER_F_UPDATE_ONLY), 431 | }, 432 | { 433 | Type: netlink.Nested | unix.WGPEER_A_ALLOWEDIPS, 434 | Data: mustAllowedIPs(peerCIPs[:ipBatchChunk]), 435 | }, 436 | }...), 437 | }), 438 | }, 439 | // Third peer, second chunk. 440 | nameAttr, 441 | { 442 | Type: netlink.Nested | unix.WGDEVICE_A_PEERS, 443 | Data: m(netlink.Attribute{ 444 | Type: netlink.Nested, 445 | Data: m([]netlink.Attribute{ 446 | { 447 | Type: unix.WGPEER_A_PUBLIC_KEY, 448 | Data: peerC[:], 449 | }, 450 | { 451 | Type: unix.WGPEER_A_FLAGS, 452 | Data: nlenc.Uint32Bytes(unix.WGPEER_F_UPDATE_ONLY), 453 | }, 454 | // Not first chunk; don't replace IPs. 455 | { 456 | Type: netlink.Nested | unix.WGPEER_A_ALLOWEDIPS, 457 | Data: mustAllowedIPs(peerCIPs[ipBatchChunk : ipBatchChunk*2]), 458 | }, 459 | }...), 460 | }), 461 | }, 462 | // Third peer, final chunk. 463 | nameAttr, 464 | { 465 | Type: netlink.Nested | unix.WGDEVICE_A_PEERS, 466 | Data: m(netlink.Attribute{ 467 | Type: netlink.Nested, 468 | Data: m([]netlink.Attribute{ 469 | { 470 | Type: unix.WGPEER_A_PUBLIC_KEY, 471 | Data: peerC[:], 472 | }, 473 | { 474 | Type: unix.WGPEER_A_FLAGS, 475 | Data: nlenc.Uint32Bytes(unix.WGPEER_F_UPDATE_ONLY), 476 | }, 477 | // Not first chunk; don't replace IPs. 478 | { 479 | Type: netlink.Nested | unix.WGPEER_A_ALLOWEDIPS, 480 | Data: mustAllowedIPs(peerCIPs[ipBatchChunk*2:]), 481 | }, 482 | }...), 483 | }), 484 | }, 485 | // Fourth peer, only chunk. 486 | nameAttr, 487 | { 488 | Type: netlink.Nested | unix.WGDEVICE_A_PEERS, 489 | Data: m(netlink.Attribute{ 490 | Type: netlink.Nested, 491 | Data: m([]netlink.Attribute{ 492 | { 493 | Type: unix.WGPEER_A_PUBLIC_KEY, 494 | Data: peerD[:], 495 | }, 496 | // Not first chunk; don't replace IPs. 497 | { 498 | Type: unix.WGPEER_A_FLAGS, 499 | Data: nlenc.Uint32Bytes(unix.WGPEER_F_REMOVE_ME), 500 | }, 501 | }...), 502 | }), 503 | }, 504 | } 505 | 506 | if diff := diffAttrs(want, allAttrs); diff != "" { 507 | t.Fatalf("unexpected final attributes (-want +got):\n%s", diff) 508 | } 509 | } 510 | 511 | func keyBytes(s string) []byte { 512 | k := wgtest.MustHexKey(s) 513 | return k[:] 514 | } 515 | 516 | func generateIPs(n int) []net.IPNet { 517 | cur, err := ipaddr.Parse("2001:db8::/64") 518 | if err != nil { 519 | panicf("failed to create cursor: %v", err) 520 | } 521 | 522 | ips := make([]net.IPNet, 0, n) 523 | for i := 0; i < n; i++ { 524 | pos := cur.Next() 525 | if pos == nil { 526 | panic("hit nil IP during IP generation") 527 | } 528 | 529 | ips = append(ips, net.IPNet{ 530 | IP: pos.IP, 531 | Mask: net.CIDRMask(128, 128), 532 | }) 533 | } 534 | 535 | return ips 536 | } 537 | --------------------------------------------------------------------------------