├── .openapi-generator ├── VERSION └── FILES ├── packaging ├── default ├── preremove.sh ├── postinstall.sh ├── preinstall.sh ├── postremove.sh ├── wgrest.service ├── nfpm-amd64.yaml ├── nfpm-arm64.yaml └── wgrest.conf ├── examples ├── qr.png └── screenshots │ ├── wgrest-add-new-peer.jpg │ ├── wgrest-devices-list.jpg │ ├── wgrest-device-peer-info.jpg │ └── wgrest-device-peers-list.jpg ├── models ├── hello-world.go ├── model_error.go ├── model_device_options.go ├── model_device_options_update_request.go ├── model_device_options_ext.go ├── model_device_create_or_update_request.go ├── model_device.go ├── model_peer_create_or_update_request.go ├── model_peer.go ├── model_device_ext.go └── model_peer_ext.go ├── logger.go ├── Dockerfile.swagger-ui ├── .dockeignore ├── .gitignore ├── utils ├── ip.go ├── interface.go ├── peers_filter.go ├── quick_config.go ├── peers_sort.go └── paginator.go ├── Dockerfile ├── storage ├── storage_test.go ├── file_storage.go └── storage.go ├── go.mod ├── handlers ├── container.go ├── utils.go └── api_device.go ├── .openapi-generator-ignore ├── Makefile ├── .drone.yml ├── cmd └── wgrest-server │ └── main.go ├── README.md ├── LICENSE ├── go.sum ├── openapi-spec.yaml └── .docs └── api └── openapi.yaml /.openapi-generator/VERSION: -------------------------------------------------------------------------------- 1 | 5.3.0 -------------------------------------------------------------------------------- /packaging/default: -------------------------------------------------------------------------------- 1 | # Default environment variables for wgrest 2 | -------------------------------------------------------------------------------- /examples/qr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suquant/wgrest/HEAD/examples/qr.png -------------------------------------------------------------------------------- /examples/screenshots/wgrest-add-new-peer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suquant/wgrest/HEAD/examples/screenshots/wgrest-add-new-peer.jpg -------------------------------------------------------------------------------- /examples/screenshots/wgrest-devices-list.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suquant/wgrest/HEAD/examples/screenshots/wgrest-devices-list.jpg -------------------------------------------------------------------------------- /examples/screenshots/wgrest-device-peer-info.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suquant/wgrest/HEAD/examples/screenshots/wgrest-device-peer-info.jpg -------------------------------------------------------------------------------- /examples/screenshots/wgrest-device-peers-list.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suquant/wgrest/HEAD/examples/screenshots/wgrest-device-peers-list.jpg -------------------------------------------------------------------------------- /packaging/preremove.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eu 3 | 4 | systemctl stop wgrest.service || true 5 | systemctl disable wgrest.service || true 6 | -------------------------------------------------------------------------------- /packaging/postinstall.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eu 3 | 4 | adduser --system wgrest --home /var/lib/wgrest 5 | 6 | systemctl enable "/etc/systemd/system/wgrest.service" 7 | -------------------------------------------------------------------------------- /packaging/preinstall.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eu 3 | 4 | if systemctl status wgrest &> /dev/null; then 5 | systemctl stop wgrest.service 6 | systemctl disable wgrest.service 7 | fi 8 | -------------------------------------------------------------------------------- /models/hello-world.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // HelloWorld is a sample data structure to make sure each endpoint return something. 4 | type HelloWorld struct { 5 | Message string `json:"message"` 6 | } -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package wgrest 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | var ( 9 | // Logger default logger 10 | Logger = log.New(os.Stderr, "", log.Ldate|log.Ltime|log.Lshortfile) 11 | ) 12 | -------------------------------------------------------------------------------- /Dockerfile.swagger-ui: -------------------------------------------------------------------------------- 1 | FROM swaggerapi/swagger-ui:v5.27.0 2 | LABEL maintainer="ForestVPN.com " 3 | 4 | COPY openapi-spec.yaml /var/www/api.spec.yaml 5 | 6 | ENV BASE_URL=/ 7 | ENV SWAGGER_JSON=/var/www/api.spec.yaml 8 | 9 | EXPOSE 8080 10 | -------------------------------------------------------------------------------- /models/model_error.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Error struct { 4 | 5 | // Error code 6 | Code string `json:"code"` 7 | 8 | // Error's short description 9 | Message string `json:"message"` 10 | 11 | // Error's detail description 12 | Detail string `json:"detail,omitempty"` 13 | } 14 | -------------------------------------------------------------------------------- /.dockeignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | dist/ 15 | .DS_Store 16 | .vscode 17 | .idea 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | dist/ 15 | clients/ 16 | .DS_Store 17 | .vscode 18 | .idea -------------------------------------------------------------------------------- /utils/ip.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | externalip "github.com/glendc/go-external-ip" 5 | ) 6 | 7 | func GetExternalIP() (string, error) { 8 | consensus := externalip.DefaultConsensus(nil, nil) 9 | ip, err := consensus.ExternalIP() 10 | if err != nil { 11 | return "", err 12 | } 13 | 14 | return ip.String(), nil 15 | } 16 | -------------------------------------------------------------------------------- /packaging/postremove.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eu 3 | 4 | function remove_user() { 5 | deluser --quiet --system wgrest >/dev/null || 6 | echo "Failed to remove user" 7 | } 8 | 9 | case $@ in 10 | # apt purge passes "purge" 11 | "purge") 12 | remove_user 13 | ;; 14 | # apt remove passes "remove" 15 | "remove") ;; 16 | 17 | esac 18 | -------------------------------------------------------------------------------- /.openapi-generator/FILES: -------------------------------------------------------------------------------- 1 | .docs/api/openapi.yaml 2 | main.go 3 | models/hello-world.go 4 | models/model_device.go 5 | models/model_device_create_or_update_request.go 6 | models/model_device_options.go 7 | models/model_device_options_update_request.go 8 | models/model_error.go 9 | models/model_peer.go 10 | models/model_peer_create_or_update_request.go 11 | -------------------------------------------------------------------------------- /packaging/wgrest.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=wgrest - REST API for WireGuard 3 | After=network.target 4 | StartLimitIntervalSec=0 5 | 6 | [Service] 7 | User=wgrest 8 | AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE 9 | EnvironmentFile=/etc/default/wgrest 10 | ExecStart=/usr/local/bin/wgrest --conf /etc/wgrest/wgrest.conf 11 | Restart=always 12 | RestartSec=1 13 | 14 | [Install] 15 | WantedBy=multi-user.target 16 | -------------------------------------------------------------------------------- /utils/interface.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "net" 4 | 5 | func GetInterfaceIPs(name string) (addresses []string, err error) { 6 | iface, err := net.InterfaceByName(name) 7 | if err != nil { 8 | return nil, err 9 | } 10 | 11 | addrs, err := iface.Addrs() 12 | if err != nil { 13 | return nil, err 14 | } 15 | 16 | for _, addr := range addrs { 17 | addresses = append(addresses, addr.String()) 18 | } 19 | 20 | return 21 | } 22 | -------------------------------------------------------------------------------- /models/model_device_options.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // DeviceOptions - Device options 4 | type DeviceOptions struct { 5 | 6 | // Device's allowed ips, it might be any of IPv4 or IPv6 addresses in CIDR notation. It might be owervrite in peer and device config. 7 | AllowedIps []string `json:"allowed_ips"` 8 | 9 | // Interface's DNS servers. 10 | DnsServers []string `json:"dns_servers"` 11 | 12 | // Device host, it might be domain name or IPv4/IPv6 address. It is used for external/internal connection 13 | Host string `json:"host"` 14 | } 15 | -------------------------------------------------------------------------------- /models/model_device_options_update_request.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // DeviceOptionsUpdateRequest - Device options 4 | type DeviceOptionsUpdateRequest struct { 5 | 6 | // Device's allowed ips, it might be any of IPv4 or IPv6 addresses in CIDR notation. It might be owervrite in peer and device config. 7 | AllowedIps *[]string `json:"allowed_ips,omitempty"` 8 | 9 | // Interface's DNS servers. 10 | DnsServers *[]string `json:"dns_servers,omitempty"` 11 | 12 | // Device host, it might be domain name or IPv4/IPv6 address. It is used for external/internal connection 13 | Host *string `json:"host,omitempty"` 14 | } 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.17.3-alpine3.14 as build-env 2 | LABEL maintainer="ForestVPN.com " 3 | 4 | RUN apk add --no-cache git gcc 5 | RUN mkdir /app 6 | 7 | WORKDIR /app 8 | 9 | COPY . . 10 | 11 | RUN export appVersion=$(git describe --tags `git rev-list -1 HEAD`) && \ 12 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ 13 | -ldflags "-X main.appVersion=$appVersion" \ 14 | -o wgrest cmd/wgrest-server/main.go 15 | 16 | FROM alpine:3.14 17 | LABEL maintainer="ForestVPN.com " 18 | 19 | COPY --from=build-env /app/wgrest . 20 | 21 | EXPOSE 8080/tcp 22 | 23 | USER 1001 24 | 25 | ENTRYPOINT ["./wgrest"] -------------------------------------------------------------------------------- /models/model_device_options_ext.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/suquant/wgrest/storage" 4 | 5 | func NewDeviceOptions(options storage.StoreDeviceOptions) DeviceOptions { 6 | return DeviceOptions{ 7 | Host: options.Host, 8 | AllowedIps: options.AllowedIPs, 9 | DnsServers: options.DNSServers, 10 | } 11 | } 12 | 13 | func (r *DeviceOptionsUpdateRequest) Apply(options *storage.StoreDeviceOptions) error { 14 | if r.Host != nil { 15 | options.Host = *r.Host 16 | } 17 | 18 | if r.DnsServers != nil { 19 | options.DNSServers = *r.DnsServers 20 | } 21 | 22 | if r.AllowedIps != nil { 23 | options.AllowedIPs = *r.AllowedIps 24 | } 25 | 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /storage/storage_test.go: -------------------------------------------------------------------------------- 1 | package storage_test 2 | 3 | import ( 4 | "github.com/suquant/wgrest/storage" 5 | "io/ioutil" 6 | "os" 7 | "testing" 8 | ) 9 | 10 | func TestStorage(t *testing.T) { 11 | t.Run("read non existing device options", func(t *testing.T) { 12 | dir, err := ioutil.TempDir("", "") 13 | if err != nil { 14 | t.Fatalf("failed to create temp dir: %s", err) 15 | } 16 | 17 | s, err := storage.NewFileStorage(dir) 18 | if err != nil { 19 | t.Fatalf("failed to create file storage: %s", err) 20 | } 21 | 22 | _, err = s.ReadDeviceOptions("xyz") 23 | if os.IsNotExist(err) != true { 24 | t.Errorf("got %s, want not exist error", err) 25 | } 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /models/model_device_create_or_update_request.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // DeviceCreateOrUpdateRequest - Device params that might be used due to creation or updation process 4 | type DeviceCreateOrUpdateRequest struct { 5 | 6 | // WireGuard device name. Usually it is network interface name 7 | Name *string `json:"name,omitempty"` 8 | 9 | // WireGuard device listen port. 10 | ListenPort *int32 `json:"listen_port,omitempty"` 11 | 12 | // WireGuard device private key encoded by base64. 13 | PrivateKey *string `json:"private_key,omitempty"` 14 | 15 | // WireGuard device firewall mark. 16 | FirewallMark *int32 `json:"firewall_mark,omitempty"` 17 | 18 | // IPv4 or IPv6 addresses in CIDR notation 19 | Networks *[]string `json:"networks,omitempty"` 20 | } 21 | -------------------------------------------------------------------------------- /packaging/nfpm-amd64.yaml: -------------------------------------------------------------------------------- 1 | name: "wgrest" 2 | arch: "amd64" 3 | platform: "linux" 4 | version: "${VERSION}" 5 | maintainer: ForestVPN.com 6 | depends: 7 | - wireguard 8 | contents: 9 | - src: dist/wgrest-linux-amd64 10 | dst: /usr/local/bin/wgrest 11 | - src: packaging/wgrest.service 12 | dst: /etc/systemd/system/wgrest.service 13 | type: config 14 | - src: packaging/default 15 | dst: /etc/default/wgrest 16 | type: config 17 | - src: packaging/wgrest.conf 18 | dst: /etc/wgrest/wgrest.conf 19 | type: config 20 | overrides: 21 | deb: 22 | scripts: 23 | preinstall: ./packaging/preinstall.sh 24 | postinstall: ./packaging/postinstall.sh 25 | preremove: ./packaging/preremove.sh 26 | postremove: ./packaging/postremove.sh 27 | -------------------------------------------------------------------------------- /packaging/nfpm-arm64.yaml: -------------------------------------------------------------------------------- 1 | name: "wgrest" 2 | arch: "arm64" 3 | platform: "linux" 4 | version: "${VERSION}" 5 | maintainer: ForestVPN.com 6 | depends: 7 | - wireguard 8 | contents: 9 | - src: dist/wgrest-linux-arm64 10 | dst: /usr/local/bin/wgrest 11 | - src: packaging/wgrest.service 12 | dst: /etc/systemd/system/wgrest.service 13 | type: config 14 | - src: packaging/default 15 | dst: /etc/default/wgrest 16 | type: config 17 | - src: packaging/wgrest.conf 18 | dst: /etc/wgrest/wgrest.conf 19 | type: config 20 | overrides: 21 | deb: 22 | scripts: 23 | preinstall: ./packaging/preinstall.sh 24 | postinstall: ./packaging/postinstall.sh 25 | preremove: ./packaging/preremove.sh 26 | postremove: ./packaging/postremove.sh 27 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/suquant/wgrest 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect 7 | github.com/glendc/go-external-ip v0.1.0 8 | github.com/labstack/echo/v4 v4.6.1 9 | github.com/labstack/gommon v0.3.1 // indirect 10 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e 11 | github.com/urfave/cli/v2 v2.3.0 12 | golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa 13 | golang.org/x/net v0.0.0-20211111160137-58aab5ef257a // indirect 14 | golang.org/x/sys v0.0.0-20211112143042-c6105e7cf70d // indirect 15 | golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect 16 | golang.zx2c4.com/wireguard v0.0.0-20211111141719-cad0ff2cfbd9 // indirect 17 | golang.zx2c4.com/wireguard/wgctrl v0.0.0-20211109202428-0073765f69ba 18 | ) 19 | -------------------------------------------------------------------------------- /handlers/container.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/suquant/wgrest/storage" 5 | ) 6 | 7 | type WireGuardContainerOptions struct { 8 | Storage storage.Storage 9 | DefaultDeviceOptions storage.StoreDeviceOptions 10 | } 11 | 12 | // WireGuardContainer will hold all dependencies for your application. 13 | type WireGuardContainer struct { 14 | storage storage.Storage 15 | defaultDeviceOptions storage.StoreDeviceOptions 16 | } 17 | 18 | // NewWireGuardContainer returns an empty or an initialized container for your handlers. 19 | func NewWireGuardContainer(options WireGuardContainerOptions) (WireGuardContainer, error) { 20 | c := WireGuardContainer{ 21 | storage: options.Storage, 22 | defaultDeviceOptions: options.DefaultDeviceOptions, 23 | } 24 | 25 | return c, nil 26 | } 27 | -------------------------------------------------------------------------------- /utils/peers_filter.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/suquant/wgrest/models" 5 | "golang.zx2c4.com/wireguard/wgctrl/wgtypes" 6 | "strings" 7 | ) 8 | 9 | func FilterPeersByQuery(q string, peers []wgtypes.Peer) []wgtypes.Peer { 10 | var filteredPeers []wgtypes.Peer 11 | for _, peer := range peers { 12 | var terms = []string{ 13 | peer.PublicKey.String(), 14 | } 15 | 16 | if peer.PresharedKey != models.EmptyKey { 17 | terms = append(terms, peer.PresharedKey.String()) 18 | } 19 | 20 | for _, v := range peer.AllowedIPs { 21 | terms = append(terms, v.String()) 22 | } 23 | 24 | if peer.Endpoint != nil { 25 | terms = append(terms, peer.Endpoint.String()) 26 | } 27 | 28 | if strings.Contains(strings.Join(terms, " "), q) { 29 | filteredPeers = append(filteredPeers, peer) 30 | } 31 | } 32 | 33 | return filteredPeers 34 | } 35 | -------------------------------------------------------------------------------- /models/model_device.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // Device - Information about wireguard device. 4 | type Device struct { 5 | 6 | // WireGuard device name. Usually it is network interface name 7 | Name string `json:"name"` 8 | 9 | // WireGuard device listen port. 10 | ListenPort int32 `json:"listen_port"` 11 | 12 | // WireGuard device public key encoded by base64. 13 | PublicKey string `json:"public_key"` 14 | 15 | // WireGuard device firewall mark. 16 | FirewallMark int32 `json:"firewall_mark"` 17 | 18 | // IPv4 or IPv6 addresses in CIDR notation 19 | Networks []string `json:"networks"` 20 | 21 | // WireGuard device's peers count 22 | PeersCount int32 `json:"peers_count"` 23 | 24 | // WireGuard device's peers total receive bytes 25 | TotalReceiveBytes int64 `json:"total_receive_bytes"` 26 | 27 | // WireGuard device's peers total transmit bytes 28 | TotalTransmitBytes int64 `json:"total_transmit_bytes"` 29 | } 30 | -------------------------------------------------------------------------------- /models/model_peer_create_or_update_request.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // PeerCreateOrUpdateRequest - Peer params that might be used due to creation or updation process 4 | type PeerCreateOrUpdateRequest struct { 5 | 6 | // Base64 encoded private key. If present it will be stored in persistent storage. 7 | PrivateKey *string `json:"private_key,omitempty"` 8 | 9 | // Base64 encoded public key 10 | PublicKey *string `json:"public_key,omitempty"` 11 | 12 | // Base64 encoded preshared key 13 | PresharedKey *string `json:"preshared_key,omitempty"` 14 | 15 | // Peer's allowed ips, it might be any of IPv4 or IPv6 addresses in CIDR notation 16 | AllowedIps *[]string `json:"allowed_ips,omitempty"` 17 | 18 | // Peer's persistend keepalive interval. Valid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\". 19 | PersistentKeepaliveInterval string `json:"persistent_keepalive_interval,omitempty"` 20 | 21 | // Peer's endpoint in host:port format 22 | Endpoint string `json:"endpoint,omitempty"` 23 | } 24 | -------------------------------------------------------------------------------- /models/model_peer.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Peer - Information about wireguard peer. 8 | type Peer struct { 9 | 10 | // Base64 encoded public key 11 | PublicKey string `json:"public_key"` 12 | 13 | // URL safe base64 encoded public key. It is usefull to use in peers api endpoint. 14 | UrlSafePublicKey string `json:"url_safe_public_key"` 15 | 16 | // Base64 encoded preshared key 17 | PresharedKey string `json:"preshared_key,omitempty"` 18 | 19 | // Peer's allowed ips, it might be any of IPv4 or IPv6 addresses in CIDR notation 20 | AllowedIps []string `json:"allowed_ips"` 21 | 22 | // Peer's last handshake time formated in RFC3339 23 | LastHandshakeTime time.Time `json:"last_handshake_time"` 24 | 25 | // Peer's persistend keepalive interval in 26 | PersistentKeepaliveInterval string `json:"persistent_keepalive_interval"` 27 | 28 | // Peer's endpoint in host:port format 29 | Endpoint string `json:"endpoint"` 30 | 31 | // Peer's receive bytes 32 | ReceiveBytes int64 `json:"receive_bytes"` 33 | 34 | // Peer's transmit bytes 35 | TransmitBytes int64 `json:"transmit_bytes"` 36 | } 37 | -------------------------------------------------------------------------------- /.openapi-generator-ignore: -------------------------------------------------------------------------------- 1 | # OpenAPI Generator Ignore 2 | # Generated by openapi-generator https://github.com/openapitools/openapi-generator 3 | 4 | # Use this file to prevent files from being overwritten by the generator. 5 | # The patterns follow closely to .gitignore or .dockerignore. 6 | 7 | # As an example, the C# client generator defines ApiClient.cs. 8 | # You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: 9 | #ApiClient.cs 10 | 11 | # You can match any string of characters against a directory, file or extension with a single asterisk (*): 12 | #foo/*/qux 13 | # The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux 14 | 15 | # You can recursively match patterns against a directory, file or extension with a double asterisk (**): 16 | #foo/**/qux 17 | # This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux 18 | 19 | # You can also negate patterns with an exclamation (!). 20 | # For example, you can ignore all files in a docs folder with the file extension .md: 21 | #docs/*.md 22 | # Then explicitly reverse the ignore rule for a single file: 23 | #!docs/README.md 24 | 25 | README.md 26 | handlers/api_device.go 27 | handlers/container.go 28 | go.mod 29 | go.sum 30 | Dockerfile -------------------------------------------------------------------------------- /models/model_device_ext.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "golang.zx2c4.com/wireguard/wgctrl/wgtypes" 4 | 5 | func NewDevice(device *wgtypes.Device) Device { 6 | var totalReceiveBytes int64 7 | var totalTransmitBytes int64 8 | 9 | for _, peer := range device.Peers { 10 | totalReceiveBytes += peer.ReceiveBytes 11 | totalTransmitBytes += peer.TransmitBytes 12 | } 13 | 14 | return Device{ 15 | Name: device.Name, 16 | ListenPort: int32(device.ListenPort), 17 | PublicKey: device.PublicKey.String(), 18 | PeersCount: int32(len(device.Peers)), 19 | FirewallMark: int32(device.FirewallMark), 20 | TotalReceiveBytes: totalReceiveBytes, 21 | TotalTransmitBytes: totalTransmitBytes, 22 | } 23 | } 24 | 25 | func (r *DeviceCreateOrUpdateRequest) Apply(conf *wgtypes.Config) error { 26 | if r.FirewallMark != nil { 27 | fwMark := int(*r.FirewallMark) 28 | conf.FirewallMark = &fwMark 29 | } 30 | 31 | if r.PrivateKey != nil { 32 | privKey, err := wgtypes.ParseKey(*r.PrivateKey) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | conf.PrivateKey = &privKey 38 | } 39 | 40 | if r.ListenPort != nil { 41 | listenPort := int(*r.ListenPort) 42 | conf.ListenPort = &listenPort 43 | } 44 | 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BUILDDIR ?= dist 2 | OSS ?= linux darwin freebsd windows 3 | ARCHS ?= amd64 arm64 4 | VERSION ?= $(shell git describe --tags `git rev-list -1 HEAD`) 5 | 6 | build: $(BUILDDIR)/wgrest 7 | 8 | clean: 9 | rm -rf "$(BUILDDIR)" 10 | 11 | install: build 12 | 13 | define wgrest 14 | $(BUILDDIR)/wgrest-$(1)-$(2): export CGO_ENABLED := 0 15 | $(BUILDDIR)/wgrest-$(1)-$(2): export GOOS := $(1) 16 | $(BUILDDIR)/wgrest-$(1)-$(2): export GOARCH := $(2) 17 | $(BUILDDIR)/wgrest-$(1)-$(2): 18 | go build \ 19 | -ldflags="-s -w -X main.appVersion=$(VERSION)" \ 20 | -trimpath -v -o "$(BUILDDIR)/wgrest-$(1)-$(2)" \ 21 | cmd/wgrest-server/main.go 22 | endef 23 | $(foreach OS,$(OSS),$(foreach ARCH,$(ARCHS),$(eval $(call wgrest,$(OS),$(ARCH))))) 24 | 25 | $(BUILDDIR)/wgrest: $(foreach OS,$(OSS),$(foreach ARCH,$(ARCHS),$(BUILDDIR)/wgrest-$(OS)-$(ARCH))) 26 | @mkdir -vp "$(BUILDDIR)" 27 | 28 | go-echo-server: 29 | openapi-generator generate -g go-echo-server \ 30 | -i openapi-spec.yaml \ 31 | -o . \ 32 | --git-host github.com \ 33 | --git-user-id suquant \ 34 | --git-repo-id wgrest 35 | 36 | typescript-axios-client: 37 | swagger-codegen generate -l typescript-axios \ 38 | --additional-properties modelPropertyNaming=original \ 39 | -i openapi-spec.yaml \ 40 | -o clients/typeascript-axios 41 | 42 | .PHONY: clean build install -------------------------------------------------------------------------------- /packaging/wgrest.conf: -------------------------------------------------------------------------------- 1 | # Listen address in format host:port. 2 | # Default is 127.0.0.1:8000 3 | listen = "127.0.0.1:8000" 4 | 5 | # Data directory. Here are stored device/peer specific options, tls certificates and more. 6 | # Default is /var/lib/wgrest 7 | data-dir = "/var/lib/wgrest" 8 | 9 | # Static auth token. It is used for bearer token authorization. When it is empty authorization is disabled. 10 | # Default is empty. 11 | static-auth-token = "" 12 | 13 | # List of domains. Used for retrieve ACME certificates. 14 | # When it is empty TLS is disabled. You can not use here wildcard "*" type domains. 15 | # Certificates are stored in data-dir/.cache directory. 16 | # Default it empty. 17 | tls-domain = [] 18 | 19 | # Demo mode. Used for demo website. 20 | # Default is disabled. 21 | demo = false 22 | 23 | # Default device allowed ips. You can overwrite it through api. 24 | # Default is 0.0.0.0/0, ::0/0 25 | device-allowed-ips = [ 26 | "0.0.0.0/0", 27 | "::0/0" 28 | ] 29 | 30 | # Default device DNS servers. You can overwrite it through api. 31 | # Default is 8.8.8.8, 1.1.1.1, 2001:4860:4860::8888, 2606:4700:4700::1111 32 | device-dns-servers = [ 33 | "8.8.8.8", 34 | "1.1.1.1", 35 | "2001:4860:4860::8888", 36 | "2606:4700:4700::1111" 37 | ] 38 | 39 | # Default device host. You can overwrite it through api 40 | # If it is empty service will try to detect default outbound IPv4 (external IPv4) and use is as default value. 41 | # Default is empty 42 | device-host = "" 43 | 44 | -------------------------------------------------------------------------------- /handlers/utils.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "github.com/labstack/echo/v4" 7 | "github.com/suquant/wgrest/models" 8 | "github.com/suquant/wgrest/utils" 9 | "golang.zx2c4.com/wireguard/wgctrl/wgtypes" 10 | "net/http" 11 | "strconv" 12 | ) 13 | 14 | func getPaginator(ctx echo.Context, nums int) (*utils.Paginator, error) { 15 | perPageParam := ctx.QueryParam("per_page") 16 | var perPage int = 100 17 | 18 | if perPageParam != "" { 19 | parsedPerPage, err := strconv.Atoi(perPageParam) 20 | if err != nil { 21 | return nil, &echo.HTTPError{ 22 | Code: http.StatusBadRequest, 23 | Message: "failed to parse per_page param", 24 | Internal: err, 25 | } 26 | } 27 | 28 | perPage = parsedPerPage 29 | } 30 | 31 | return utils.NewPaginator(ctx.Request(), perPage, nums), nil 32 | } 33 | 34 | func parseUrlSafeKey(encodedKey string) (wgtypes.Key, error) { 35 | decodedKey, err := base64.URLEncoding.DecodeString(encodedKey) 36 | if err != nil { 37 | return wgtypes.Key{}, fmt.Errorf("failed to parse key: %s", err) 38 | } 39 | 40 | if len(decodedKey) != wgtypes.KeyLen { 41 | return wgtypes.Key{}, fmt.Errorf("failed to parse key: wrong length") 42 | } 43 | var key wgtypes.Key 44 | copy(key[:32], decodedKey[:]) 45 | 46 | return key, nil 47 | } 48 | 49 | func applyNetworks(device *models.Device) error { 50 | addresses, err := utils.GetInterfaceIPs(device.Name) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | device.Networks = addresses 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /utils/quick_config.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/suquant/wgrest/models" 7 | "golang.zx2c4.com/wireguard/wgctrl/wgtypes" 8 | "io" 9 | "strings" 10 | ) 11 | 12 | type PeerQuickConfigOptions struct { 13 | PrivateKey *string 14 | DNSServers *[]string 15 | AllowedIPs *[]string 16 | Host *string 17 | } 18 | 19 | func GetPeerQuickConfig(device wgtypes.Device, peer wgtypes.Peer, options PeerQuickConfigOptions) (io.Reader, error) { 20 | b := &bytes.Buffer{} 21 | fmt.Fprintln(b, "[Interface]") 22 | if options.PrivateKey != nil { 23 | fmt.Fprintln(b, "PrivateKey =", *options.PrivateKey) 24 | } 25 | 26 | addresses := make([]string, len(peer.AllowedIPs)) 27 | for i, v := range peer.AllowedIPs { 28 | addresses[i] = v.String() 29 | } 30 | 31 | fmt.Fprintf(b, "Address = %s\n", strings.Join(addresses, ",")) 32 | if options.DNSServers != nil && len(*options.DNSServers) > 0 { 33 | fmt.Fprintf(b, "DNS = %s\n", strings.Join(*options.DNSServers, ",")) 34 | } 35 | 36 | fmt.Fprintln(b, "") 37 | fmt.Fprintln(b, "[Peer]") 38 | 39 | fmt.Fprintf(b, "PublicKey = %s\n", device.PublicKey.String()) 40 | if peer.PresharedKey != models.EmptyKey { 41 | fmt.Fprintf(b, "PresharedKey = %s\n", peer.PresharedKey.String()) 42 | } 43 | if options.Host != nil { 44 | fmt.Fprintf(b, "Endpoint = %s:%v\n", *options.Host, device.ListenPort) 45 | } 46 | if options.AllowedIPs != nil && len(*options.AllowedIPs) > 0 { 47 | fmt.Fprintf(b, "AllowedIPs = %s\n", strings.Join(*options.AllowedIPs, ",")) 48 | } 49 | 50 | return b, nil 51 | } 52 | -------------------------------------------------------------------------------- /storage/file_storage.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "encoding/base64" 5 | "golang.zx2c4.com/wireguard/wgctrl/wgtypes" 6 | "io/ioutil" 7 | "os" 8 | "path" 9 | ) 10 | 11 | type FileStorage struct { 12 | dir string 13 | } 14 | 15 | func NewFileStorage(dir string) (Storage, error) { 16 | confDir := path.Join(dir, "v1") 17 | 18 | return &FileStorage{ 19 | dir: confDir, 20 | }, os.MkdirAll(confDir, os.ModePerm) 21 | } 22 | 23 | func (s *FileStorage) getFilePath(name string) string { 24 | return path.Join(s.dir, name+".conf") 25 | } 26 | 27 | func (s *FileStorage) WriteDeviceOptions(name string, options StoreDeviceOptions) error { 28 | filePath := s.getFilePath(name) 29 | f, err := ioutil.TempFile(s.dir, name) 30 | if err != nil { 31 | return err 32 | } 33 | defer f.Close() 34 | 35 | if err := options.Dump(f); err != nil { 36 | return err 37 | } 38 | 39 | if err := os.Chmod(f.Name(), 0600); err != nil { 40 | return err 41 | } 42 | 43 | return os.Rename(f.Name(), filePath) 44 | } 45 | 46 | func (s *FileStorage) WritePeerOptions(pubKey wgtypes.Key, options StorePeerOptions) error { 47 | safeName := base64.URLEncoding.EncodeToString(pubKey[:]) 48 | filePath := s.getFilePath(safeName) 49 | f, err := ioutil.TempFile(s.dir, safeName) 50 | if err != nil { 51 | return err 52 | } 53 | defer f.Close() 54 | 55 | if err := options.Dump(f); err != nil { 56 | return err 57 | } 58 | 59 | if err := os.Chmod(f.Name(), 0600); err != nil { 60 | return err 61 | } 62 | 63 | return os.Rename(f.Name(), filePath) 64 | } 65 | 66 | func (s *FileStorage) ReadDeviceOptions(name string) (*StoreDeviceOptions, error) { 67 | f, err := os.OpenFile(s.getFilePath(name), os.O_RDONLY, 0600) 68 | if err != nil { 69 | return nil, err 70 | } 71 | defer f.Close() 72 | 73 | o := &StoreDeviceOptions{} 74 | return o, o.Restore(f) 75 | } 76 | 77 | func (s *FileStorage) ReadPeerOptions(pubKey wgtypes.Key) (*StorePeerOptions, error) { 78 | safeName := base64.URLEncoding.EncodeToString(pubKey[:]) 79 | f, err := os.OpenFile(s.getFilePath(safeName), os.O_RDONLY, 0600) 80 | if err != nil { 81 | return nil, err 82 | } 83 | defer f.Close() 84 | 85 | o := &StorePeerOptions{} 86 | return o, o.Restore(f) 87 | } 88 | -------------------------------------------------------------------------------- /models/model_peer_ext.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "golang.zx2c4.com/wireguard/wgctrl/wgtypes" 7 | "net" 8 | "time" 9 | ) 10 | 11 | var ( 12 | EmptyKey = wgtypes.Key{} 13 | ) 14 | 15 | func NewPeer(peer wgtypes.Peer) Peer { 16 | allowedIPs := make([]string, len(peer.AllowedIPs)) 17 | for i, v := range peer.AllowedIPs { 18 | allowedIPs[i] = v.String() 19 | } 20 | 21 | p := Peer{ 22 | PublicKey: peer.PublicKey.String(), 23 | UrlSafePublicKey: base64.URLEncoding.EncodeToString(peer.PublicKey[:]), 24 | AllowedIps: allowedIPs, 25 | LastHandshakeTime: peer.LastHandshakeTime, 26 | ReceiveBytes: peer.ReceiveBytes, 27 | TransmitBytes: peer.TransmitBytes, 28 | PersistentKeepaliveInterval: peer.PersistentKeepaliveInterval.String(), 29 | } 30 | 31 | if peer.PresharedKey != EmptyKey { 32 | p.PresharedKey = peer.PresharedKey.String() 33 | } 34 | 35 | if peer.Endpoint != nil { 36 | p.Endpoint = peer.Endpoint.String() 37 | } 38 | 39 | return p 40 | } 41 | 42 | func (r *PeerCreateOrUpdateRequest) Apply(conf *wgtypes.PeerConfig) error { 43 | if r.Endpoint != "" { 44 | endpoint, err := net.ResolveUDPAddr("udp", r.Endpoint) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | conf.Endpoint = endpoint 50 | } 51 | 52 | if r.PersistentKeepaliveInterval != "" { 53 | keepaliveInterval, err := time.ParseDuration(r.PersistentKeepaliveInterval) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | conf.PersistentKeepaliveInterval = &keepaliveInterval 59 | } 60 | 61 | if r.AllowedIps != nil { 62 | allowedIPs := make([]net.IPNet, len(*r.AllowedIps)) 63 | for i, v := range *r.AllowedIps { 64 | _, ipNet, err := net.ParseCIDR(v) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | if ipNet == nil { 70 | return fmt.Errorf("failed to parse CIDR: %s", v) 71 | } 72 | 73 | allowedIPs[i] = *ipNet 74 | } 75 | 76 | conf.AllowedIPs = allowedIPs 77 | } 78 | 79 | if r.PresharedKey != nil { 80 | psKey, err := wgtypes.ParseKey(*r.PresharedKey) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | conf.PresharedKey = &psKey 86 | } 87 | 88 | if r.PrivateKey != nil { 89 | privKey, err := wgtypes.ParseKey(*r.PrivateKey) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | if privKey.PublicKey() != conf.PublicKey { 95 | return fmt.Errorf("wrong private key") 96 | } 97 | } 98 | 99 | return nil 100 | } 101 | -------------------------------------------------------------------------------- /storage/storage.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "golang.zx2c4.com/wireguard/wgctrl/wgtypes" 7 | "io" 8 | "strings" 9 | ) 10 | 11 | type StoreDeviceOptions struct { 12 | DNSServers []string 13 | AllowedIPs []string 14 | Host string 15 | } 16 | 17 | func (o *StoreDeviceOptions) Dump(w io.Writer) error { 18 | fmt.Fprintf(w, "DNS = %s\n", strings.Join(o.DNSServers, ", ")) 19 | fmt.Fprintf(w, "AllowedIPs = %s\n", strings.Join(o.AllowedIPs, ", ")) 20 | fmt.Fprintf(w, "Host = %s\n", o.Host) 21 | 22 | return nil 23 | } 24 | 25 | func (o *StoreDeviceOptions) Restore(r io.Reader) error { 26 | scanner := bufio.NewScanner(r) 27 | scanner.Split(bufio.ScanLines) 28 | for scanner.Scan() { 29 | line := scanner.Text() 30 | terms := strings.SplitN(line, "=", 2) 31 | if len(terms) != 2 { 32 | return fmt.Errorf("failed to parse line: %s", line) 33 | } 34 | 35 | left := strings.ToLower(strings.TrimSpace(terms[0])) 36 | switch left { 37 | case "dns": 38 | right := strings.Split(terms[1], ",") 39 | dnsServers := make([]string, len(right)) 40 | for i, v := range right { 41 | dnsServers[i] = strings.TrimSpace(v) 42 | } 43 | o.DNSServers = dnsServers 44 | break 45 | case "allowedips": 46 | right := strings.Split(terms[1], ",") 47 | for _, v := range right { 48 | o.AllowedIPs = append(o.AllowedIPs, strings.TrimSpace(v)) 49 | } 50 | break 51 | case "host": 52 | o.Host = strings.TrimSpace(terms[1]) 53 | break 54 | default: 55 | break 56 | } 57 | } 58 | 59 | return nil 60 | } 61 | 62 | type StorePeerOptions struct { 63 | PrivateKey string 64 | } 65 | 66 | func (o *StorePeerOptions) Dump(w io.Writer) error { 67 | fmt.Fprintf(w, "PrivateKey = %s\n", o.PrivateKey) 68 | 69 | return nil 70 | } 71 | 72 | func (o *StorePeerOptions) Restore(r io.Reader) error { 73 | scanner := bufio.NewScanner(r) 74 | scanner.Split(bufio.ScanLines) 75 | for scanner.Scan() { 76 | line := scanner.Text() 77 | terms := strings.SplitN(line, "=", 2) 78 | if len(terms) != 2 { 79 | return fmt.Errorf("failed to parse line: %s", line) 80 | } 81 | 82 | left := strings.ToLower(strings.TrimSpace(terms[0])) 83 | switch left { 84 | case "privatekey": 85 | o.PrivateKey = strings.TrimSpace(terms[1]) 86 | break 87 | default: 88 | return fmt.Errorf("invalid option: %s", left) 89 | } 90 | } 91 | 92 | return nil 93 | } 94 | 95 | type Storage interface { 96 | WriteDeviceOptions(name string, options StoreDeviceOptions) error 97 | WritePeerOptions(pubKey wgtypes.Key, options StorePeerOptions) error 98 | 99 | ReadDeviceOptions(name string) (*StoreDeviceOptions, error) 100 | ReadPeerOptions(pubKey wgtypes.Key) (*StorePeerOptions, error) 101 | } 102 | -------------------------------------------------------------------------------- /.drone.yml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: pipeline 3 | type: docker 4 | name: default 5 | 6 | workspace: 7 | path: /drone/src 8 | 9 | volumes: 10 | - name: cache 11 | temp: { } 12 | - name: dist 13 | temp: { } 14 | 15 | environment: 16 | GOCACHE: /cache/go/build 17 | GOMODCACHE: /cache/go/download 18 | NPM_CONFIG_CACHE: /cache/npm/cache 19 | 20 | steps: 21 | - name: deps 22 | image: golang:1.17.3-alpine3.14 23 | volumes: 24 | - name: cache 25 | path: /cache 26 | commands: 27 | - apk --no-cache add git 28 | - go get -t -d -v ./... 29 | - go build all 30 | 31 | - name: build 32 | image: golang:1.17.3-alpine3.14 33 | privileged: true 34 | volumes: 35 | - name: cache 36 | path: /cache 37 | - name: dist 38 | path: /drone/src/dist 39 | commands: 40 | - export VERSION=${DRONE_TAG:-${DRONE_COMMIT:0:7}} 41 | - apk --no-cache add build-base 42 | - make build 43 | depends_on: 44 | - deps 45 | 46 | - name: test 47 | image: golang:1.17.3-alpine3.14 48 | volumes: 49 | - name: cache 50 | path: /cache 51 | - name: dist 52 | path: /drone/src/dist 53 | commands: 54 | - apk --no-cache add build-base 55 | - go test -race -coverprofile=/cache/coverage.txt -covermode=atomic ./... 56 | depends_on: 57 | - deps 58 | 59 | - name: coverage 60 | image: alpine:3.14.3 61 | volumes: 62 | - name: cache 63 | path: /cache 64 | environment: 65 | CODECOV_TOKEN: 66 | from_secret: CODECOV_TOKEN 67 | commands: 68 | - apk --no-cache add bash curl 69 | - curl -L https://codecov.io/bash -o /usr/local/bin/codecov 70 | - chmod +x /usr/local/bin/codecov 71 | - /usr/local/bin/codecov -f /cache/coverage.txt 72 | depends_on: 73 | - test 74 | 75 | - name: build packages 76 | image: alpine:3.14.3 77 | volumes: 78 | - name: dist 79 | path: /drone/src/dist 80 | commands: 81 | - export VERSION=${DRONE_TAG:-${DRONE_COMMIT:0:7}} 82 | - apk --no-cache add nfpm 83 | - nfpm pkg -f packaging/nfpm-amd64.yaml --packager deb --target dist/wgrest_amd64.deb 84 | - nfpm pkg -f packaging/nfpm-arm64.yaml --packager deb --target dist/wgrest_arm64.deb 85 | depends_on: 86 | - build 87 | 88 | - name: github release 89 | image: plugins/github-release 90 | volumes: 91 | - name: dist 92 | path: /drone/src/dist 93 | settings: 94 | api_key: 95 | from_secret: github_token 96 | files: dist/* 97 | checksum: 98 | - md5 99 | - sha1 100 | depends_on: 101 | - build packages 102 | when: 103 | event: 104 | - tag 105 | 106 | trigger: 107 | event: 108 | - push 109 | - tag 110 | -------------------------------------------------------------------------------- /utils/peers_sort.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "golang.zx2c4.com/wireguard/wgctrl/wgtypes" 7 | "sort" 8 | ) 9 | 10 | type sortPeerByPubKey []wgtypes.Peer 11 | 12 | func (a sortPeerByPubKey) Len() int { return len(a) } 13 | func (a sortPeerByPubKey) Less(i, j int) bool { 14 | return bytes.Compare(a[i].PublicKey[:], a[j].PublicKey[:]) > 0 15 | } 16 | func (a sortPeerByPubKey) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 17 | 18 | type sortPeerByReceiveBytes []wgtypes.Peer 19 | 20 | func (a sortPeerByReceiveBytes) Len() int { return len(a) } 21 | func (a sortPeerByReceiveBytes) Less(i, j int) bool { return a[i].ReceiveBytes < a[j].ReceiveBytes } 22 | func (a sortPeerByReceiveBytes) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 23 | 24 | type sortPeerByReceiveBytesDesc []wgtypes.Peer 25 | 26 | func (a sortPeerByReceiveBytesDesc) Len() int { return len(a) } 27 | func (a sortPeerByReceiveBytesDesc) Less(i, j int) bool { return a[i].ReceiveBytes > a[j].ReceiveBytes } 28 | func (a sortPeerByReceiveBytesDesc) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 29 | 30 | type sortPeerByTransmitBytes []wgtypes.Peer 31 | 32 | func (a sortPeerByTransmitBytes) Len() int { return len(a) } 33 | func (a sortPeerByTransmitBytes) Less(i, j int) bool { return a[i].ReceiveBytes < a[j].ReceiveBytes } 34 | func (a sortPeerByTransmitBytes) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 35 | 36 | type sortPeerByTransmitBytesDesc []wgtypes.Peer 37 | 38 | func (a sortPeerByTransmitBytesDesc) Len() int { return len(a) } 39 | func (a sortPeerByTransmitBytesDesc) Less(i, j int) bool { 40 | return a[i].ReceiveBytes < a[j].ReceiveBytes 41 | } 42 | func (a sortPeerByTransmitBytesDesc) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 43 | 44 | type sortPeerByTotalBytes []wgtypes.Peer 45 | 46 | func (a sortPeerByTotalBytes) Len() int { return len(a) } 47 | func (a sortPeerByTotalBytes) Less(i, j int) bool { 48 | return a[i].ReceiveBytes+a[i].TransmitBytes < a[j].ReceiveBytes+a[j].TransmitBytes 49 | } 50 | func (a sortPeerByTotalBytes) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 51 | 52 | type sortPeerByTotalBytesDesc []wgtypes.Peer 53 | 54 | func (a sortPeerByTotalBytesDesc) Len() int { return len(a) } 55 | func (a sortPeerByTotalBytesDesc) Less(i, j int) bool { 56 | return a[i].ReceiveBytes+a[i].TransmitBytes > a[j].ReceiveBytes+a[j].TransmitBytes 57 | } 58 | func (a sortPeerByTotalBytesDesc) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 59 | 60 | type sortPeerByLastHandshakeTime []wgtypes.Peer 61 | 62 | func (a sortPeerByLastHandshakeTime) Len() int { return len(a) } 63 | func (a sortPeerByLastHandshakeTime) Less(i, j int) bool { 64 | return a[i].LastHandshakeTime.Before(a[j].LastHandshakeTime) 65 | } 66 | func (a sortPeerByLastHandshakeTime) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 67 | 68 | type sortPeerByLastHandshakeTimeDesc []wgtypes.Peer 69 | 70 | func (a sortPeerByLastHandshakeTimeDesc) Len() int { return len(a) } 71 | func (a sortPeerByLastHandshakeTimeDesc) Less(i, j int) bool { 72 | return a[i].LastHandshakeTime.After(a[j].LastHandshakeTime) 73 | } 74 | func (a sortPeerByLastHandshakeTimeDesc) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 75 | 76 | func SortPeersByField(field string, peers []wgtypes.Peer) error { 77 | 78 | switch field { 79 | case "pub_key": 80 | sort.Sort(sortPeerByPubKey(peers)) 81 | break 82 | case "receive_bytes": 83 | sort.Sort(sortPeerByReceiveBytes(peers)) 84 | break 85 | case "-receive_bytes": 86 | sort.Sort(sortPeerByReceiveBytesDesc(peers)) 87 | break 88 | case "transmit_bytes": 89 | sort.Sort(sortPeerByTransmitBytes(peers)) 90 | break 91 | case "-transmit_bytes": 92 | sort.Sort(sortPeerByTransmitBytesDesc(peers)) 93 | break 94 | case "total_bytes": 95 | sort.Sort(sortPeerByTotalBytes(peers)) 96 | break 97 | case "-total_bytes": 98 | sort.Sort(sortPeerByTotalBytesDesc(peers)) 99 | break 100 | case "last_handshake_time": 101 | sort.Sort(sortPeerByLastHandshakeTime(peers)) 102 | break 103 | case "-last_handshake_time": 104 | sort.Sort(sortPeerByLastHandshakeTimeDesc(peers)) 105 | break 106 | default: 107 | return fmt.Errorf("wrong sort field: %s", field) 108 | } 109 | 110 | return nil 111 | } 112 | -------------------------------------------------------------------------------- /utils/paginator.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // copyright by https://github.com/astaxie/beego/blob/v1.12.3/utils/pagination/paginator.go 4 | 5 | import ( 6 | "fmt" 7 | "github.com/labstack/echo/v4" 8 | "math" 9 | "net/http" 10 | "net/url" 11 | "reflect" 12 | "strconv" 13 | "strings" 14 | ) 15 | 16 | // Paginator within the state of a http request. 17 | type Paginator struct { 18 | Request *http.Request 19 | PerPageNums int 20 | MaxPages int 21 | 22 | nums int64 23 | pageRange []int 24 | pageNums int 25 | page int 26 | } 27 | 28 | // PageNums Returns the total number of pages. 29 | func (p *Paginator) PageNums() int { 30 | if p.pageNums != 0 { 31 | return p.pageNums 32 | } 33 | pageNums := math.Ceil(float64(p.nums) / float64(p.PerPageNums)) 34 | if p.MaxPages > 0 { 35 | pageNums = math.Min(pageNums, float64(p.MaxPages)) 36 | } 37 | p.pageNums = int(pageNums) 38 | return p.pageNums 39 | } 40 | 41 | // Nums Returns the total number of items (e.g. from doing SQL count). 42 | func (p *Paginator) Nums() int64 { 43 | return p.nums 44 | } 45 | 46 | // SetNums Sets the total number of items. 47 | func (p *Paginator) SetNums(nums interface{}) { 48 | p.nums, _ = toInt64(nums) 49 | } 50 | 51 | // Page Returns the current page. 52 | func (p *Paginator) Page() int { 53 | if p.page != 0 { 54 | return p.page 55 | } 56 | if p.Request.Form == nil { 57 | p.Request.ParseForm() 58 | } 59 | p.page, _ = strconv.Atoi(p.Request.Form.Get("page")) 60 | if p.page > p.PageNums() { 61 | p.page = p.PageNums() 62 | } 63 | if p.page <= 0 { 64 | p.page = 1 65 | } 66 | return p.page 67 | } 68 | 69 | // Pages Returns a list of all pages. 70 | // 71 | // Usage (in a view template): 72 | // 73 | // {{range $index, $page := .paginator.Pages}} 74 | // 75 | // {{$page}} 76 | // 77 | // {{end}} 78 | func (p *Paginator) Pages() []int { 79 | if p.pageRange == nil && p.nums > 0 { 80 | var pages []int 81 | pageNums := p.PageNums() 82 | page := p.Page() 83 | switch { 84 | case page >= pageNums-4 && pageNums > 9: 85 | start := pageNums - 9 + 1 86 | pages = make([]int, 9) 87 | for i := range pages { 88 | pages[i] = start + i 89 | } 90 | case page >= 5 && pageNums > 9: 91 | start := page - 5 + 1 92 | pages = make([]int, int(math.Min(9, float64(page+4+1)))) 93 | for i := range pages { 94 | pages[i] = start + i 95 | } 96 | default: 97 | pages = make([]int, int(math.Min(9, float64(pageNums)))) 98 | for i := range pages { 99 | pages[i] = i + 1 100 | } 101 | } 102 | p.pageRange = pages 103 | } 104 | return p.pageRange 105 | } 106 | 107 | // PageLink Returns URL for a given page index. 108 | func (p *Paginator) PageLink(page int) string { 109 | link, _ := url.ParseRequestURI(p.Request.URL.String()) 110 | values := link.Query() 111 | if page == 1 { 112 | values.Del("page") 113 | } else { 114 | values.Set("page", strconv.Itoa(page)) 115 | } 116 | link.RawQuery = values.Encode() 117 | return link.String() 118 | } 119 | 120 | // PageLinkPrev Returns URL to the previous page. 121 | func (p *Paginator) PageLinkPrev() (link string) { 122 | if p.HasPrev() { 123 | link = p.PageLink(p.Page() - 1) 124 | } 125 | return 126 | } 127 | 128 | // PageLinkNext Returns URL to the next page. 129 | func (p *Paginator) PageLinkNext() (link string) { 130 | if p.HasNext() { 131 | link = p.PageLink(p.Page() + 1) 132 | } 133 | return 134 | } 135 | 136 | // PageLinkFirst Returns URL to the first page. 137 | func (p *Paginator) PageLinkFirst() (link string) { 138 | return p.PageLink(1) 139 | } 140 | 141 | // PageLinkLast Returns URL to the last page. 142 | func (p *Paginator) PageLinkLast() (link string) { 143 | return p.PageLink(p.PageNums()) 144 | } 145 | 146 | // HasPrev Returns true if the current page has a predecessor. 147 | func (p *Paginator) HasPrev() bool { 148 | return p.Page() > 1 149 | } 150 | 151 | // HasNext Returns true if the current page has a successor. 152 | func (p *Paginator) HasNext() bool { 153 | return p.Page() < p.PageNums() 154 | } 155 | 156 | // IsActive Returns true if the given page index points to the current page. 157 | func (p *Paginator) IsActive(page int) bool { 158 | return p.Page() == page 159 | } 160 | 161 | // Offset Returns the current offset. 162 | func (p *Paginator) Offset() int { 163 | return (p.Page() - 1) * p.PerPageNums 164 | } 165 | 166 | // HasPages Returns true if there is more than one page. 167 | func (p *Paginator) HasPages() bool { 168 | return p.PageNums() > 1 169 | } 170 | 171 | func (p *Paginator) Write(response *echo.Response) { 172 | var links []string = []string{ 173 | fmt.Sprintf( 174 | "<%s>; rel=\"first\"", 175 | p.PageLinkFirst(), 176 | ), 177 | fmt.Sprintf( 178 | "<%s>; rel=\"last\"", 179 | p.PageLinkLast(), 180 | ), 181 | } 182 | 183 | if p.HasNext() { 184 | links = append(links, fmt.Sprintf( 185 | "<%s>; rel=\"next\"", 186 | p.PageLinkNext(), 187 | )) 188 | } 189 | 190 | if p.HasPrev() { 191 | links = append(links, fmt.Sprintf( 192 | "<%s>; rel=\"prev\"", 193 | p.PageLinkPrev(), 194 | )) 195 | } 196 | 197 | response.Header().Set( 198 | "Link", strings.Join(links, ",")) 199 | } 200 | 201 | // NewPaginator Instantiates a paginator struct for the current http request. 202 | func NewPaginator(req *http.Request, per int, nums interface{}) *Paginator { 203 | p := Paginator{} 204 | p.Request = req 205 | if per <= 0 { 206 | per = 10 207 | } 208 | p.PerPageNums = per 209 | p.SetNums(nums) 210 | return &p 211 | } 212 | 213 | // ToInt64 convert any numeric value to int64 214 | func toInt64(value interface{}) (d int64, err error) { 215 | val := reflect.ValueOf(value) 216 | switch value.(type) { 217 | case int, int8, int16, int32, int64: 218 | d = val.Int() 219 | case uint, uint8, uint16, uint32, uint64: 220 | d = int64(val.Uint()) 221 | default: 222 | err = fmt.Errorf("ToInt64 need numeric not `%T`", value) 223 | } 224 | return 225 | } 226 | -------------------------------------------------------------------------------- /cmd/wgrest-server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/labstack/echo/v4" 6 | "github.com/labstack/echo/v4/middleware" 7 | "github.com/suquant/wgrest/handlers" 8 | "github.com/suquant/wgrest/storage" 9 | "github.com/suquant/wgrest/utils" 10 | "github.com/urfave/cli/v2" 11 | "github.com/urfave/cli/v2/altsrc" 12 | "golang.org/x/crypto/acme/autocert" 13 | "log" 14 | "net/http" 15 | "os" 16 | "path" 17 | ) 18 | 19 | var ( 20 | appVersion string // Populated during build time 21 | ) 22 | 23 | func main() { 24 | flags := []cli.Flag{ 25 | &cli.StringFlag{ 26 | Name: "conf", 27 | Value: "/etc/wgrest/wgrest.conf", 28 | Usage: "wgrest config file path", 29 | EnvVars: []string{"WGREST_CONF"}, 30 | }, 31 | &cli.BoolFlag{ 32 | Name: "version", 33 | Value: false, 34 | Usage: "Print version and exit", 35 | }, 36 | altsrc.NewStringFlag(&cli.StringFlag{ 37 | Name: "listen", 38 | Value: "127.0.0.1:8000", 39 | Usage: "Listen address", 40 | EnvVars: []string{"WGREST_LISTEN"}, 41 | }), 42 | altsrc.NewStringFlag(&cli.StringFlag{ 43 | Name: "data-dir", 44 | Value: "/var/lib/wgrest", 45 | Usage: "Data dir", 46 | EnvVars: []string{"WGREST_DATA_DIR"}, 47 | }), 48 | altsrc.NewStringFlag(&cli.StringFlag{ 49 | Name: "static-auth-token", 50 | Value: "", 51 | Usage: "It is used for bearer token authorization", 52 | EnvVars: []string{"WGREST_STATIC_AUTH_TOKEN"}, 53 | }), 54 | altsrc.NewStringSliceFlag(&cli.StringSliceFlag{ 55 | Name: "tls-domain", 56 | Value: cli.NewStringSlice(), 57 | Usage: "TLS Domains", 58 | EnvVars: []string{"WGREST_TLS_DOMAIN"}, 59 | }), 60 | altsrc.NewBoolFlag(&cli.BoolFlag{ 61 | Name: "demo", 62 | Value: false, 63 | Usage: "Demo mode", 64 | EnvVars: []string{"WGREST_DEMO"}, 65 | }), 66 | altsrc.NewStringSliceFlag(&cli.StringSliceFlag{ 67 | Name: "device-allowed-ips", 68 | Value: cli.NewStringSlice("0.0.0.0/0", "::0/0"), 69 | Usage: "Default device allowed ips. You can overwrite it through api", 70 | EnvVars: []string{"WGREST_DEVICE_ALLOWED_IPS"}, 71 | }), 72 | altsrc.NewStringSliceFlag(&cli.StringSliceFlag{ 73 | Name: "device-dns-servers", 74 | Value: cli.NewStringSlice("8.8.8.8", "1.1.1.1", "2001:4860:4860::8888", "2606:4700:4700::1111"), 75 | Usage: "Default device DNS servers. You can overwrite it through api", 76 | EnvVars: []string{"WGREST_DEVICE_DNS_SERVERS"}, 77 | }), 78 | altsrc.NewStringFlag(&cli.StringFlag{ 79 | Name: "device-host", 80 | Value: "", 81 | Usage: "Default device host. You can overwrite it through api", 82 | EnvVars: []string{"WGREST_DEVICE_HOST"}, 83 | }), 84 | } 85 | 86 | app := &cli.App{ 87 | Name: "wgrest", 88 | Usage: "wgrest - rest api for wireguard", 89 | Flags: flags, 90 | Before: altsrc.InitInputSourceWithContext(flags, altsrc.NewTomlSourceFromFlagFunc("conf")), 91 | Action: func(c *cli.Context) error { 92 | if c.Bool("version") { 93 | fmt.Printf("wgrest version: %s\n", appVersion) 94 | return nil 95 | } 96 | 97 | e := echo.New() 98 | e.HideBanner = true 99 | 100 | // Basic middleware 101 | e.Use(middleware.Logger()) 102 | e.Use(middleware.Recover()) 103 | e.Pre(middleware.Rewrite(map[string]string{ 104 | "^/devices": "/", 105 | "^/devices/*": "/", 106 | })) 107 | 108 | e.GET("/version", getVersionHandler) 109 | 110 | dataDir := c.String("data-dir") 111 | e.File("/", path.Join(dataDir, "public", "index.html")) 112 | e.Static("/", path.Join(dataDir, "public")) 113 | 114 | cacheDir := path.Join(dataDir, ".cache") 115 | tlsDomains := c.StringSlice("tls-domain") 116 | if len(tlsDomains) > 0 { 117 | e.AutoTLSManager.HostPolicy = autocert.HostWhitelist(tlsDomains...) 118 | e.AutoTLSManager.Cache = autocert.DirCache(cacheDir) 119 | } 120 | 121 | v1 := e.Group("/v1") 122 | v1.Use(middleware.CORSWithConfig(middleware.CORSConfig{ 123 | Skipper: middleware.DefaultSkipper, 124 | AllowOrigins: []string{"*"}, 125 | AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete}, 126 | AllowHeaders: []string{"Content-Type", "Accept", "Accept-Language", "Link", "Authorization"}, 127 | AllowCredentials: true, 128 | })) 129 | 130 | staticAuthToken := c.String("static-auth-token") 131 | if staticAuthToken != "" { 132 | v1.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) { 133 | return key == staticAuthToken, nil 134 | })) 135 | } 136 | 137 | wgStorage, err := storage.NewFileStorage(dataDir) 138 | if err != nil { 139 | return err 140 | } 141 | 142 | defaultDeviceHost := c.String("device-host") 143 | if defaultDeviceHost == "" { 144 | defaultDeviceHost, err = utils.GetExternalIP() 145 | if err != nil { 146 | log.Printf("failed to identify external ip: %s", err.Error()) 147 | } 148 | } 149 | 150 | defaultDeviceOptions := storage.StoreDeviceOptions{ 151 | AllowedIPs: c.StringSlice("device-allowed-ips"), 152 | DNSServers: c.StringSlice("device-dns-servers"), 153 | Host: defaultDeviceHost, 154 | } 155 | 156 | wc, err := handlers.NewWireGuardContainer(handlers.WireGuardContainerOptions{ 157 | Storage: wgStorage, 158 | DefaultDeviceOptions: defaultDeviceOptions, 159 | }) 160 | if err != nil { 161 | return err 162 | } 163 | 164 | // CreateDevice - Create new device 165 | v1.POST("/devices/", wc.CreateDevice) 166 | 167 | // CreateDevicePeer - Create new device peer 168 | v1.POST("/devices/:name/peers/", wc.CreateDevicePeer) 169 | 170 | // DeleteDevice - Delete Device 171 | v1.DELETE("/devices/:name/", wc.DeleteDevice) 172 | 173 | // DeleteDevicePeer - Delete device's peer 174 | v1.DELETE("/devices/:name/peers/:urlSafePubKey/", wc.DeleteDevicePeer) 175 | 176 | // GetDevice - Get device info 177 | v1.GET("/devices/:name/", wc.GetDevice) 178 | 179 | // GetDevicePeer - Get device peer info 180 | v1.GET("/devices/:name/peers/:urlSafePubKey/", wc.GetDevicePeer) 181 | 182 | // ListDevicePeers - Peers list 183 | v1.GET("/devices/:name/peers/", wc.ListDevicePeers) 184 | 185 | // ListDevices - Devices list 186 | v1.GET("/devices/", wc.ListDevices) 187 | 188 | // UpdateDevice - Update device 189 | v1.PATCH("/devices/:name/", wc.UpdateDevice) 190 | 191 | // UpdateDevicePeer - Update device's peer 192 | v1.PATCH("/devices/:name/peers/:urlSafePubKey/", wc.UpdateDevicePeer) 193 | 194 | // GetDevicePeerQuickConfig - Get device peer quick config 195 | v1.GET("/devices/:name/peers/:urlSafePubKey/quick.conf", wc.GetDevicePeerQuickConfig) 196 | 197 | // GetDevicePeerQuickConfigQRCodePNG - Get device peer quick config QR code 198 | v1.GET("/devices/:name/peers/:urlSafePubKey/quick.conf.png", wc.GetDevicePeerQuickConfigQRCodePNG) 199 | 200 | // GetDeviceOptions - Get device options 201 | v1.GET("/devices/:name/options/", wc.GetDeviceOptions) 202 | 203 | // UpdateDeviceOptions - Update device's options 204 | v1.PATCH("/devices/:name/options/", wc.UpdateDeviceOptions) 205 | 206 | listen := c.String("listen") 207 | // Start server 208 | if len(tlsDomains) > 0 { 209 | return e.StartAutoTLS(listen) 210 | } else { 211 | return e.Start(listen) 212 | } 213 | }, 214 | } 215 | 216 | err := app.Run(os.Args) 217 | if err != nil { 218 | log.Fatal(err) 219 | } 220 | } 221 | 222 | func getVersionHandler(ctx echo.Context) error { 223 | return ctx.JSON(http.StatusOK, appVersion) 224 | } 225 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | WGRest 2 | --- 3 | [![Build Status](https://drone.forestvpn.com/api/badges/suquant/wgrest/status.svg)](https://drone.forestvpn.com/suquant/wgrest) 4 | [![codecov](https://codecov.io/gh/suquant/wgrest/branch/master/graph/badge.svg?token=NM179YJFEJ)](https://codecov.io/gh/suquant/wgrest) 5 | 6 | WGRest is a WireGuard REST API server. It operates wireguard through IPC and doesn't require any dependencies. It aims 7 | to be simpler, faster, and usable on embedded devices such as routers or any other low power and low memory devices. 8 | 9 | WireGuard is a simple and modern VPN. It is cross-platform (Windows, macOS, BSD, iOS, Android). 10 | 11 | Swagger UI: https://wgrest.forestvpn.com/swagger/ 12 | 13 | 1|2|3|4 14 | :---:|:---:|:---:|:---: 15 | [![Devices list](examples/screenshots/wgrest-devices-list.jpg)](examples/screenshots/wgrest-devices-list.jpg) | [![Device's peers list](examples/screenshots/wgrest-device-peers-list.jpg)](examples/screenshots/wgrest-device-peers-list.jpg) | [![Device's peers list](examples/screenshots/wgrest-device-peer-info.jpg)](examples/screenshots/wgrest-device-peer-info.jpg) | [![Add new peer](examples/screenshots/wgrest-add-new-peer.jpg)](examples/screenshots/wgrest-add-new-peer.jpg) 16 | 17 | ## Features: 18 | 19 | * Manage device: update wireguard interface 20 | * Manage device's peers: create, update, and delete peers 21 | * Peer's QR code, for use in WireGuard & ForestVPN client 22 | * Peers search by query 23 | * Peers sort by: pub_key, receive_bytes, transmit_bytes, total_bytes, last_handshake_time 24 | * ACME TLS support 25 | * Bearer token auth 26 | 27 | Check all features [here](https://wgrest.forestvpn.com/swagger/) 28 | 29 | ## Install 30 | 31 | ### On Debian / Ubuntu 32 | 33 | #### WGRest server 34 | 35 | ```shell 36 | curl -L https://github.com/suquant/wgrest/releases/latest/download/wgrest_amd64.deb -o wgrest_amd64.deb 37 | 38 | dpkg -i wgrest_amd64.deb 39 | ``` 40 | 41 | #### WGRest Web App 42 | 43 | ```shell 44 | curl -L https://github.com/suquant/wgrest-webapp/releases/latest/download/wgrest-webapp_amd64.deb -o wgrest-webapp_amd64.deb 45 | 46 | dpkg -i wgrest-webapp_amd64.deb 47 | ``` 48 | 49 | ### Manual 50 | 51 | WGRest optionally comes with web ui and it is not included by default into binary. You need to do some extra actions to 52 | enable it. 53 | 54 | ```shell 55 | curl -L https://github.com/suquant/wgrest/releases/latest/download/wgrest-linux-amd64 -o wgrest 56 | 57 | chmod +x wgrest 58 | ``` 59 | 60 | ```shell 61 | wgrest -h 62 | 63 | NAME: 64 | wgrest - wgrest - rest api for wireguard 65 | 66 | USAGE: 67 | wgrest [global options] command [command options] [arguments...] 68 | 69 | COMMANDS: 70 | help, h Shows a list of commands or help for one command 71 | 72 | GLOBAL OPTIONS: 73 | --conf value wgrest config file path (default: "/etc/wgrest/wgrest.conf") [$WGREST_CONF] 74 | --version Print version and exit (default: false) 75 | --listen value Listen address (default: "127.0.0.1:8000") [$WGREST_LISTEN] 76 | --data-dir value Data dir (default: "/var/lib/wgrest") [$WGREST_DATA_DIR] 77 | --static-auth-token value It is used for bearer token authorization [$WGREST_STATIC_AUTH_TOKEN] 78 | --tls-domain value TLS Domains [$WGREST_TLS_DOMAIN] 79 | --demo Demo mode (default: false) [$WGREST_DEMO] 80 | --device-allowed-ips value Default device allowed ips. You can overwrite it through api (default: "0.0.0.0/0", "::0/0") [$WGREST_DEVICE_ALLOWED_IPS] 81 | --device-dns-servers value Default device DNS servers. You can overwrite it through api (default: "8.8.8.8", "1.1.1.1", "2001:4860:4860::8888", "2606:4700:4700::1111") [$WGREST_DEVICE_DNS_SERVERS] 82 | --device-host value Default device host. You can overwrite it through api [$WGREST_DEVICE_HOST] 83 | --help, -h show help (default: false) 84 | ``` 85 | 86 | For Web UI support you need to: 87 | 88 | ```shell 89 | curl -L https://github.com/suquant/wgrest-webapp/releases/latest/download/webapp.tar.gz -o webapp.tar.gz 90 | 91 | sudo mkdir -p /var/lib/wgrest/ 92 | sudo chown `whoami` /var/lib/wgrest/ 93 | tar -xzvf webapp.tar.gz -C /var/lib/wgrest/ 94 | ``` 95 | 96 | After run the server web ui will be available at [http://127.0.0.1:8000/](http://127.0.0.1:8000/) 97 | 98 | ## Run WireGuard REST API Server 99 | 100 | ```shell 101 | wgrest --static-auth-token "secret" --listen "127.0.0.1:8000" 102 | ``` 103 | 104 | ```shell 105 | Output: 106 | 107 | ⇨ http server started on 127.0.0.1:8000 108 | ``` 109 | 110 | ## Update **wg0** device 111 | 112 | ```shell 113 | curl -v -g \ 114 | -H "Content-Type: application/json" \ 115 | -H "Authorization: Bearer secret" \ 116 | -X PATCH \ 117 | -d '{ 118 | "listen_port":51820, 119 | "private_key": "cLmxIyJx/PGWrQlevBGr2LQNOqmBGYbVfu4XcRO2SEo=" 120 | }' \ 121 | http://127.0.0.1:8000/v1/devices/wg0/ 122 | ``` 123 | 124 | ```json 125 | { 126 | "name": "wg0", 127 | "listen_port": 51820, 128 | "public_key": "7TvriTzbaXdrsGXI8oMrMoNAWrVCXRUfiEvksOewLyg=", 129 | "firewall_mark": 0, 130 | "networks": null, 131 | "peers_count": 7, 132 | "total_receive_bytes": 0, 133 | "total_transmit_bytes": 0 134 | } 135 | ``` 136 | 137 | ## Get devices 138 | 139 | ```shell 140 | curl -v -g \ 141 | -H "Content-Type: application/json" \ 142 | -H "Authorization: Bearer secret" \ 143 | -X GET \ 144 | http://127.0.0.1:8000/v1/devices/ 145 | ``` 146 | 147 | ```json 148 | [ 149 | { 150 | "name": "wg0", 151 | "listen_port": 51820, 152 | "public_key": "7TvriTzbaXdrsGXI8oMrMoNAWrVCXRUfiEvksOewLyg=", 153 | "firewall_mark": 0, 154 | "networks": null, 155 | "peers_count": 7, 156 | "total_receive_bytes": 0, 157 | "total_transmit_bytes": 0 158 | } 159 | ] 160 | ``` 161 | 162 | ## Add peer 163 | 164 | ```shell 165 | curl -v -g \ 166 | -H "Content-Type: application/json" \ 167 | -H "Authorization: Bearer secret" \ 168 | -X POST \ 169 | -d '{ 170 | "allowed_ips": ["10.10.1.2/32"], 171 | "preshared_key": "uhFI9c9rInyxqgZfeejte6apHWbewoiy32+Bo34xRFs=" 172 | }' \ 173 | http://127.0.0.1:8000/v1/devices/wg0/peers/ 174 | ``` 175 | 176 | ```json 177 | { 178 | "public_key": "zTCuhw7g4Q7YVH6xpCjrz48UJ7qqJBwrXUpuofUTzD8=", 179 | "url_safe_public_key": "zTCuhw7g4Q7YVH6xpCjrz48UJ7qqJBwrXUpuofUTzD8=", 180 | "preshared_key": "uhFI9c9rInyxqgZfeejte6apHWbewoiy32+Bo34xRFs=", 181 | "allowed_ips": [ 182 | "10.10.1.2/32" 183 | ], 184 | "last_handshake_time": "0001-01-01T00:00:00Z", 185 | "persistent_keepalive_interval": "0s", 186 | "endpoint": "", 187 | "receive_bytes": 0, 188 | "transmit_bytes": 0 189 | } 190 | ``` 191 | 192 | ## Get peers 193 | 194 | ```shell 195 | curl -v -g \ 196 | -H "Content-Type: application/json" \ 197 | -H "Authorization: Bearer secret" \ 198 | -X GET \ 199 | http://127.0.0.1:8000/v1/devices/wg0/peers/ 200 | ``` 201 | 202 | ```json 203 | [ 204 | { 205 | "public_key": "zTCuhw7g4Q7YVH6xpCjrz48UJ7qqJBwrXUpuofUTzD8=", 206 | "url_safe_public_key": "zTCuhw7g4Q7YVH6xpCjrz48UJ7qqJBwrXUpuofUTzD8=", 207 | "preshared_key": "uhFI9c9rInyxqgZfeejte6apHWbewoiy32+Bo34xRFs=", 208 | "allowed_ips": [ 209 | "10.10.1.2/32" 210 | ], 211 | "last_handshake_time": "0001-01-01T00:00:00Z", 212 | "persistent_keepalive_interval": "0s", 213 | "endpoint": "", 214 | "receive_bytes": 0, 215 | "transmit_bytes": 0 216 | } 217 | ] 218 | ``` 219 | 220 | ## Get peer's quick config QR code 221 | 222 | ```shell 223 | curl -v -g \ 224 | -H "Content-Type: application/json" \ 225 | -H "Authorization: Bearer secret" \ 226 | -X GET \ 227 | http://127.0.0.1:8000/v1/devices/wg0/peers/zTCuhw7g4Q7YVH6xpCjrz48UJ7qqJBwrXUpuofUTzD8=/quick.conf.png?width=256 228 | ``` 229 | 230 | ![QR Code](examples/qr.png) 231 | 232 | ## Delete peer 233 | 234 | Since the wireguard public key is the standard base64 encoded string, it is not safe to use in URI schema, is that 235 | reason peer_id contains the same public key of the peer but encoded with URL safe base64 encoder. 236 | 237 | peer_id can be retrieved either by `peer_id` field from peer list endpoint or by this rule 238 | 239 | ```shell 240 | python3 -c "import base64; \ 241 | print(\ 242 | base64.urlsafe_b64encode(\ 243 | base64.b64decode('hQ1yeyFy+bZn/5jpQNNrZ8MTIGaimZxT6LbWAkvmKjA=')\ 244 | ).decode()\ 245 | )" 246 | ``` 247 | 248 | delete peer request 249 | 250 | ```shell 251 | curl -v -g \ 252 | -H "Content-Type: application/json" \ 253 | -H "Authorization: Bearer secret" \ 254 | -X DELETE \ 255 | http://127.0.0.1:8000/v1/devices/wg0/peers/ 256 | ``` 257 | 258 | Credits: 259 | 260 | - ForestVPN.com [Free VPN](https://forestvpn.com) for all 261 | - SpaceV.net [VPN for teams](https://spacev.net) 262 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/cilium/ebpf v0.5.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= 4 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 5 | github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= 6 | github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= 11 | github.com/glendc/go-external-ip v0.1.0 h1:iX3xQ2Q26atAmLTbd++nUce2P5ht5P4uD4V7caSY/xg= 12 | github.com/glendc/go-external-ip v0.1.0/go.mod h1:CNx312s2FLAJoWNdJWZ2Fpf5O4oLsMFwuYviHjS4uJE= 13 | github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= 14 | github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 15 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 16 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 17 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 18 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 19 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 20 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 21 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 22 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 23 | github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 h1:uhL5Gw7BINiiPAo24A2sxkcDI0Jt/sqp1v5xQCniEFA= 24 | github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= 25 | github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= 26 | github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ= 27 | github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok= 28 | github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391/go.mod h1:cR77jAZG3Y3bsb8hF6fHJbFoyFukLFOkQ98S0pQz3xw= 29 | github.com/jsimonetti/rtnetlink v0.0.0-20201220180245-69540ac93943/go.mod h1:z4c53zj6Eex712ROyh8WI0ihysb5j2ROyV42iNogmAs= 30 | github.com/jsimonetti/rtnetlink v0.0.0-20210122163228-8d122574c736/go.mod h1:ZXpIyOK59ZnN7J0BV99cZUPmsqDRZ3eq5X+st7u/oSA= 31 | github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b/go.mod h1:8w9Rh8m+aHZIG69YPGGem1i5VzoyRC8nw2kA8B+ik5U= 32 | github.com/jsimonetti/rtnetlink v0.0.0-20210525051524-4cc836578190 h1:iycCSDo8EKVueI9sfVBBJmtNn9DnXV/K1YWwEJO+uOs= 33 | github.com/jsimonetti/rtnetlink v0.0.0-20210525051524-4cc836578190/go.mod h1:NmKSdU4VGSiv1bMsdqNALI4RSvvjtz65tTMCnD05qLo= 34 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 35 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 36 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 37 | github.com/labstack/echo/v4 v4.6.1 h1:OMVsrnNFzYlGSdaiYGHbgWQnr+JM7NG+B9suCPie14M= 38 | github.com/labstack/echo/v4 v4.6.1/go.mod h1:RnjgMWNDB9g/HucVWhQYNQP9PvbYf6adqftqryo7s9k= 39 | github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= 40 | github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o= 41 | github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= 42 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 43 | github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 44 | github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs= 45 | github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 46 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 47 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 48 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 49 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 50 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 51 | github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43 h1:WgyLFv10Ov49JAQI/ZLUkCZ7VJS3r74hwFIGXJsgZlY= 52 | github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo= 53 | github.com/mdlayher/genetlink v1.0.0 h1:OoHN1OdyEIkScEmRgxLEe2M9U8ClMytqA5niynLtfj0= 54 | github.com/mdlayher/genetlink v1.0.0/go.mod h1:0rJ0h4itni50A86M2kHcgS85ttZazNt7a8H2a2cw0Gc= 55 | github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA= 56 | github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M= 57 | github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY= 58 | github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o= 59 | github.com/mdlayher/netlink v1.2.0/go.mod h1:kwVW1io0AZy9A1E2YYgaD4Cj+C+GPkU6klXCMzIJ9p8= 60 | github.com/mdlayher/netlink v1.2.1/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU= 61 | github.com/mdlayher/netlink v1.2.2-0.20210123213345-5cc92139ae3e/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU= 62 | github.com/mdlayher/netlink v1.3.0/go.mod h1:xK/BssKuwcRXHrtN04UBkwQ6dY9VviGGuriDdoPSWys= 63 | github.com/mdlayher/netlink v1.4.0/go.mod h1:dRJi5IABcZpBD2A3D0Mv/AiX8I9uDEu5oGkAVrekmf8= 64 | github.com/mdlayher/netlink v1.4.1 h1:I154BCU+mKlIf7BgcAJB2r7QjveNPty6uNY1g9ChVfI= 65 | github.com/mdlayher/netlink v1.4.1/go.mod h1:e4/KuJ+s8UhfUpO9z00/fDZZmhSrs+oxyqAS9cNgn6Q= 66 | github.com/mdlayher/socket v0.0.0-20210307095302-262dc9984e00/go.mod h1:GAFlyu4/XV68LkQKYzKhIo/WW7j3Zi0YRAz/BOoanUc= 67 | github.com/mdlayher/socket v0.0.0-20211102153432-57e3fa563ecb h1:2dC7L10LmTqlyMVzFJ00qM25lqESg9Z4u3GuEXN5iHY= 68 | github.com/mdlayher/socket v0.0.0-20211102153432-57e3fa563ecb/go.mod h1:nFZ1EtZYK8Gi/k6QNu7z7CgO20i/4ExeQswwWuPmG/g= 69 | github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws= 70 | github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc= 71 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 72 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 73 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 74 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 75 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 76 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 77 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= 78 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= 79 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 80 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 81 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 82 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 83 | github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= 84 | github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= 85 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 86 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 87 | github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= 88 | github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4= 89 | github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 90 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 91 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 92 | golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 93 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 94 | golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa h1:idItI2DDfCokpg0N51B2VtiLdJ4vAuXC9fnCb2gACo4= 95 | golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 96 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 97 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 98 | golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 99 | golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 100 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 101 | golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 102 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 103 | golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 104 | golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 105 | golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 106 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 107 | golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 108 | golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 109 | golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 110 | golang.org/x/net v0.0.0-20211105192438-b53810dc28af/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 111 | golang.org/x/net v0.0.0-20211108170745-6635138e15ea/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 112 | golang.org/x/net v0.0.0-20211111083644-e5c967477495/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 113 | golang.org/x/net v0.0.0-20211111160137-58aab5ef257a h1:c83jeVQW0KGKNaKBRfelNYNHaev+qawl9yaA825s8XE= 114 | golang.org/x/net v0.0.0-20211111160137-58aab5ef257a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 115 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 116 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 117 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 118 | golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 119 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 120 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 121 | golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 122 | golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 123 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 124 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 125 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 126 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 127 | golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 128 | golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 129 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 130 | golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 131 | golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 132 | golang.org/x/sys v0.0.0-20210123111255-9b0068b26619/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 133 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 134 | golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 135 | golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 136 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 137 | golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 138 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 139 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 140 | golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 141 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 142 | golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 143 | golang.org/x/sys v0.0.0-20211106132015-ebca88c72f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 144 | golang.org/x/sys v0.0.0-20211109184856-51b60fd695b3/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 145 | golang.org/x/sys v0.0.0-20211110154304-99a53858aa08/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 146 | golang.org/x/sys v0.0.0-20211112143042-c6105e7cf70d h1:jp6PtFmjL+vGsuzd86xYqaJGv6eXdLvmVGzVVLI6EPI= 147 | golang.org/x/sys v0.0.0-20211112143042-c6105e7cf70d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 148 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 149 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 150 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 151 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 152 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 153 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 154 | golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 155 | golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= 156 | golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 157 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 158 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 159 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 160 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 161 | golang.zx2c4.com/go118/netip v0.0.0-20211106132939-9d41d90554dd/go.mod h1:5yyfuiqVIJ7t+3MqrpTQ+QqRkMWiESiyDvPNvKYCecg= 162 | golang.zx2c4.com/go118/netip v0.0.0-20211111135330-a4a02eeacf9d/go.mod h1:5yyfuiqVIJ7t+3MqrpTQ+QqRkMWiESiyDvPNvKYCecg= 163 | golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= 164 | golang.zx2c4.com/wireguard v0.0.0-20211109020618-685490f568cf/go.mod h1:M4ViMFJSFA3E7DEM9lS9NsCbDhurQdVjipAqa06Jy2Q= 165 | golang.zx2c4.com/wireguard v0.0.0-20211111141719-cad0ff2cfbd9 h1:hgOsyFPweYvWdi8aSR7C0tr368oQjCuxuRBKHKpJdUA= 166 | golang.zx2c4.com/wireguard v0.0.0-20211111141719-cad0ff2cfbd9/go.mod h1:TjUWrnD5ATh7bFvmm/ALEJZQ4ivKbETb6pmyj1vUoNI= 167 | golang.zx2c4.com/wireguard/wgctrl v0.0.0-20211109202428-0073765f69ba h1:YUZtxQfFidpvaaVWGk22eGEvVdtTY5SRDLuL996sr5k= 168 | golang.zx2c4.com/wireguard/wgctrl v0.0.0-20211109202428-0073765f69ba/go.mod h1:fhr0mQ0d0pUv+gK1R0HBTPPh7a86/48ugXX6q0jSru8= 169 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 170 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 171 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 172 | gopkg.in/yaml.v2 v2.2.3 h1:fvjTMHxHEw/mxHbtzPi3JCcKXQRAnQTBRo6YCJSVHKI= 173 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 174 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 175 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 176 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 177 | -------------------------------------------------------------------------------- /handlers/api_device.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "bytes" 5 | "github.com/labstack/echo/v4" 6 | "github.com/skip2/go-qrcode" 7 | "github.com/suquant/wgrest/models" 8 | "github.com/suquant/wgrest/storage" 9 | "github.com/suquant/wgrest/utils" 10 | "golang.zx2c4.com/wireguard/wgctrl" 11 | "golang.zx2c4.com/wireguard/wgctrl/wgtypes" 12 | "io" 13 | "net/http" 14 | "net/url" 15 | "os" 16 | "strconv" 17 | ) 18 | 19 | // CreateDevice - Create new device 20 | // @todo: need to be implemented 21 | func (c *WireGuardContainer) CreateDevice(ctx echo.Context) error { 22 | var request models.DeviceCreateOrUpdateRequest 23 | if err := ctx.Bind(&request); err != nil { 24 | return err 25 | } 26 | 27 | return ctx.NoContent(http.StatusNotImplemented) 28 | } 29 | 30 | // CreateDevicePeer - Create new device peer 31 | func (c *WireGuardContainer) CreateDevicePeer(ctx echo.Context) error { 32 | var request models.PeerCreateOrUpdateRequest 33 | if err := ctx.Bind(&request); err != nil { 34 | return err 35 | } 36 | 37 | var privateKey *wgtypes.Key 38 | peerConf := wgtypes.PeerConfig{} 39 | if request.PublicKey != nil { 40 | pubKey, err := wgtypes.ParseKey(*request.PublicKey) 41 | if err != nil { 42 | ctx.Logger().Errorf("failed to parse public key: %s", err) 43 | return ctx.JSON(http.StatusInternalServerError, models.Error{ 44 | Code: "wireguard_config_error", 45 | Message: err.Error(), 46 | }) 47 | } 48 | 49 | peerConf.PublicKey = pubKey 50 | } else if request.PrivateKey != nil { 51 | privKey, err := wgtypes.ParseKey(*request.PrivateKey) 52 | if err != nil { 53 | ctx.Logger().Errorf("failed to parse private key: %s", err) 54 | return ctx.JSON(http.StatusInternalServerError, models.Error{ 55 | Code: "wireguard_config_error", 56 | Message: err.Error(), 57 | }) 58 | } 59 | 60 | peerConf.PublicKey = privKey.PublicKey() 61 | privateKey = &privKey 62 | } else { 63 | privKey, err := wgtypes.GeneratePrivateKey() 64 | if err != nil { 65 | ctx.Logger().Errorf("failed to generate private key: %s", err) 66 | return ctx.JSON(http.StatusInternalServerError, models.Error{ 67 | Code: "wireguard_config_error", 68 | Message: err.Error(), 69 | }) 70 | } 71 | 72 | peerConf.PublicKey = privKey.PublicKey() 73 | privateKey = &privKey 74 | } 75 | 76 | if privateKey != nil { 77 | err := c.storage.WritePeerOptions(peerConf.PublicKey, storage.StorePeerOptions{ 78 | PrivateKey: privateKey.String(), 79 | }) 80 | 81 | if err != nil { 82 | ctx.Logger().Errorf("failed to save peer options: %s", err) 83 | return ctx.JSON(http.StatusInternalServerError, models.Error{ 84 | Code: "wireguard_config_error", 85 | Message: err.Error(), 86 | }) 87 | } 88 | } 89 | 90 | name := ctx.Param("name") 91 | 92 | client, err := wgctrl.New() 93 | if err != nil { 94 | ctx.Logger().Errorf("failed to init wireguard ipc: %s", err) 95 | return ctx.JSON(http.StatusInternalServerError, models.Error{ 96 | Code: "wireguard_client_error", 97 | Message: err.Error(), 98 | }) 99 | } 100 | defer client.Close() 101 | 102 | _, err = client.Device(name) 103 | if err != nil { 104 | if os.IsNotExist(err) { 105 | return ctx.NoContent(http.StatusNotFound) 106 | } 107 | 108 | ctx.Logger().Errorf("failed to get wireguard device: %s", err) 109 | return ctx.JSON(http.StatusInternalServerError, models.Error{ 110 | Code: "wireguard_device_error", 111 | Message: err.Error(), 112 | }) 113 | } 114 | 115 | err = request.Apply(&peerConf) 116 | if err != nil { 117 | ctx.Logger().Errorf("failed to init wireguard ipc: %s", err) 118 | return ctx.JSON(http.StatusBadRequest, models.Error{ 119 | Code: "wireguard_config_error", 120 | Message: err.Error(), 121 | }) 122 | } 123 | 124 | deviceConf := wgtypes.Config{ 125 | Peers: []wgtypes.PeerConfig{ 126 | peerConf, 127 | }, 128 | } 129 | 130 | if err := client.ConfigureDevice(name, deviceConf); err != nil { 131 | ctx.Logger().Errorf("failed to configure wireguard device(%s): %s", name, err) 132 | return ctx.JSON(http.StatusBadRequest, models.Error{ 133 | Code: "wireguard_error", 134 | Message: err.Error(), 135 | }) 136 | } 137 | 138 | device, err := client.Device(name) 139 | if err != nil { 140 | if os.IsNotExist(err) { 141 | return ctx.NoContent(http.StatusNotFound) 142 | } 143 | 144 | ctx.Logger().Errorf("failed to get wireguard device: %s", err) 145 | return ctx.JSON(http.StatusInternalServerError, models.Error{ 146 | Code: "wireguard_device_error", 147 | Message: err.Error(), 148 | }) 149 | } 150 | 151 | var peer wgtypes.Peer 152 | for _, v := range device.Peers { 153 | if v.PublicKey == peerConf.PublicKey { 154 | peer = v 155 | break 156 | } 157 | } 158 | 159 | return ctx.JSON(http.StatusCreated, models.NewPeer(peer)) 160 | } 161 | 162 | // DeleteDevice - Delete Device 163 | // @todo: need to be implemented 164 | func (c *WireGuardContainer) DeleteDevice(ctx echo.Context) error { 165 | return ctx.NoContent(http.StatusNotImplemented) 166 | } 167 | 168 | // DeleteDevicePeer - Delete device's peer 169 | func (c *WireGuardContainer) DeleteDevicePeer(ctx echo.Context) error { 170 | name := ctx.Param("name") 171 | urlSafePubKey, err := url.QueryUnescape(ctx.Param("urlSafePubKey")) 172 | if err != nil { 173 | ctx.Logger().Errorf("failed to parse pub key: %s", err) 174 | return ctx.JSON(http.StatusBadRequest, models.Error{ 175 | Code: "request_params_error", 176 | Message: err.Error(), 177 | }) 178 | } 179 | 180 | pubKey, err := parseUrlSafeKey(urlSafePubKey) 181 | if err != nil { 182 | ctx.Logger().Errorf("failed to parse pub key: %s", err) 183 | return ctx.JSON(http.StatusBadRequest, models.Error{ 184 | Code: "request_params_error", 185 | Message: err.Error(), 186 | }) 187 | } 188 | 189 | client, err := wgctrl.New() 190 | if err != nil { 191 | ctx.Logger().Errorf("failed to init wireguard ipc: %s", err) 192 | return ctx.JSON(http.StatusInternalServerError, models.Error{ 193 | Code: "wireguard_client_error", 194 | Message: err.Error(), 195 | }) 196 | } 197 | defer client.Close() 198 | 199 | _, err = client.Device(name) 200 | if err != nil { 201 | if os.IsNotExist(err) { 202 | return ctx.NoContent(http.StatusNotFound) 203 | } 204 | 205 | ctx.Logger().Errorf("failed to get wireguard device: %s", err) 206 | return ctx.JSON(http.StatusInternalServerError, models.Error{ 207 | Code: "wireguard_device_error", 208 | Message: err.Error(), 209 | }) 210 | } 211 | 212 | deviceConf := wgtypes.Config{ 213 | Peers: []wgtypes.PeerConfig{ 214 | wgtypes.PeerConfig{ 215 | PublicKey: pubKey, 216 | Remove: true, 217 | }, 218 | }, 219 | } 220 | 221 | if err := client.ConfigureDevice(name, deviceConf); err != nil { 222 | ctx.Logger().Errorf("failed to configure wireguard device(%s): %s", name, err) 223 | return ctx.JSON(http.StatusBadRequest, models.Error{ 224 | Code: "wireguard_error", 225 | Message: err.Error(), 226 | }) 227 | } 228 | 229 | return ctx.NoContent(http.StatusNoContent) 230 | } 231 | 232 | // GetDevice - Get device info 233 | func (c *WireGuardContainer) GetDevice(ctx echo.Context) error { 234 | name := ctx.Param("name") 235 | 236 | client, err := wgctrl.New() 237 | if err != nil { 238 | ctx.Logger().Errorf("failed to init wireguard ipc: %s", err) 239 | return ctx.JSON(http.StatusInternalServerError, models.Error{ 240 | Code: "wireguard_client_error", 241 | Message: err.Error(), 242 | }) 243 | } 244 | defer client.Close() 245 | 246 | device, err := client.Device(name) 247 | if err != nil { 248 | if os.IsNotExist(err) { 249 | return ctx.NoContent(http.StatusNotFound) 250 | } 251 | 252 | ctx.Logger().Errorf("failed to get wireguard device: %s", err) 253 | return ctx.JSON(http.StatusInternalServerError, models.Error{ 254 | Code: "wireguard_device_error", 255 | Message: err.Error(), 256 | }) 257 | } 258 | 259 | result := models.NewDevice(device) 260 | if err := applyNetworks(&result); err != nil { 261 | ctx.Logger().Errorf("failed to get networks for interface %s: %s", result.Name, err) 262 | return ctx.JSON(http.StatusInternalServerError, models.Error{ 263 | Code: "wireguard_device_error", 264 | Message: err.Error(), 265 | }) 266 | } 267 | 268 | return ctx.JSON(http.StatusOK, result) 269 | } 270 | 271 | // GetDevicePeer - Get device peer info 272 | func (c *WireGuardContainer) GetDevicePeer(ctx echo.Context) error { 273 | name := ctx.Param("name") 274 | urlSafePubKey, err := url.QueryUnescape(ctx.Param("urlSafePubKey")) 275 | if err != nil { 276 | ctx.Logger().Errorf("failed to parse pub key: %s", err) 277 | return ctx.JSON(http.StatusBadRequest, models.Error{ 278 | Code: "request_params_error", 279 | Message: err.Error(), 280 | }) 281 | } 282 | pubKey, err := parseUrlSafeKey(urlSafePubKey) 283 | if err != nil { 284 | ctx.Logger().Errorf("failed to parse pub key: %s", err) 285 | return ctx.JSON(http.StatusBadRequest, models.Error{ 286 | Code: "request_params_error", 287 | Message: err.Error(), 288 | }) 289 | } 290 | 291 | client, err := wgctrl.New() 292 | if err != nil { 293 | ctx.Logger().Errorf("failed to init wireguard ipc: %s", err) 294 | return ctx.JSON(http.StatusInternalServerError, models.Error{ 295 | Code: "wireguard_client_error", 296 | Message: err.Error(), 297 | }) 298 | } 299 | defer client.Close() 300 | 301 | device, err := client.Device(name) 302 | if err != nil { 303 | if os.IsNotExist(err) { 304 | return ctx.NoContent(http.StatusNotFound) 305 | } 306 | 307 | ctx.Logger().Errorf("failed to get wireguard device: %s", err) 308 | return ctx.JSON(http.StatusInternalServerError, models.Error{ 309 | Code: "wireguard_device_error", 310 | Message: err.Error(), 311 | }) 312 | } 313 | 314 | var peer *wgtypes.Peer 315 | for _, v := range device.Peers { 316 | if v.PublicKey == pubKey { 317 | peer = &v 318 | break 319 | } 320 | } 321 | 322 | if peer == nil { 323 | return ctx.NoContent(http.StatusNotFound) 324 | } 325 | 326 | return ctx.JSON(http.StatusOK, models.NewPeer(*peer)) 327 | } 328 | 329 | // ListDevicePeers - Peers list 330 | func (c *WireGuardContainer) ListDevicePeers(ctx echo.Context) error { 331 | name := ctx.Param("name") 332 | 333 | client, err := wgctrl.New() 334 | if err != nil { 335 | ctx.Logger().Errorf("failed to init wireguard ipc: %s", err) 336 | return ctx.JSON(http.StatusInternalServerError, models.Error{ 337 | Code: "wireguard_client_error", 338 | Message: err.Error(), 339 | }) 340 | } 341 | defer client.Close() 342 | 343 | device, err := client.Device(name) 344 | if err != nil { 345 | if os.IsNotExist(err) { 346 | return ctx.NoContent(http.StatusNotFound) 347 | } 348 | 349 | ctx.Logger().Errorf("failed to get wireguard device: %s", err) 350 | return ctx.JSON(http.StatusInternalServerError, models.Error{ 351 | Code: "wireguard_device_error", 352 | Message: err.Error(), 353 | }) 354 | } 355 | 356 | filteredPeers := device.Peers 357 | q := ctx.QueryParam("q") 358 | if q != "" { 359 | filteredPeers = utils.FilterPeersByQuery(q, filteredPeers) 360 | } 361 | 362 | sortField := ctx.QueryParam("sort") 363 | if sortField != "" { 364 | if err := utils.SortPeersByField(sortField, filteredPeers); err != nil { 365 | ctx.Logger().Errorf("failed sort paginatedPeers: %s", err) 366 | return ctx.JSON(http.StatusBadRequest, models.Error{ 367 | Code: "request_params_error", 368 | Message: err.Error(), 369 | }) 370 | } 371 | } 372 | 373 | paginator, err := getPaginator(ctx, len(filteredPeers)) 374 | if err != nil { 375 | ctx.Logger().Errorf("failed to init paginator: %s", err) 376 | return err 377 | } 378 | 379 | beginIndex := paginator.Offset() 380 | endIndex := beginIndex + paginator.PerPageNums 381 | if int64(beginIndex) > paginator.Nums() { 382 | beginIndex = int(paginator.Nums()) 383 | } 384 | if int64(endIndex) > paginator.Nums() { 385 | endIndex = int(paginator.Nums()) 386 | } 387 | 388 | paginatedPeers := filteredPeers[beginIndex:endIndex] 389 | result := make([]models.Peer, len(paginatedPeers)) 390 | for i, v := range paginatedPeers { 391 | result[i] = models.NewPeer(v) 392 | } 393 | 394 | paginator.Write(ctx.Response()) 395 | return ctx.JSON(http.StatusOK, result) 396 | } 397 | 398 | // ListDevices - Devices list 399 | func (c *WireGuardContainer) ListDevices(ctx echo.Context) error { 400 | client, err := wgctrl.New() 401 | if err != nil { 402 | ctx.Logger().Errorf("failed to init wireguard ipc: %s", err) 403 | return ctx.JSON(http.StatusInternalServerError, models.Error{ 404 | Code: "wireguard_client_error", 405 | Message: err.Error(), 406 | }) 407 | } 408 | defer client.Close() 409 | 410 | devices, err := client.Devices() 411 | if err != nil { 412 | ctx.Logger().Errorf("failed to get wireguard devices: %s", err) 413 | return ctx.JSON(http.StatusInternalServerError, models.Error{ 414 | Code: "wireguard_client_error", 415 | Message: err.Error(), 416 | }) 417 | } 418 | 419 | paginator, err := getPaginator(ctx, len(devices)) 420 | if err != nil { 421 | ctx.Logger().Errorf("failed to init paginator: %s", err) 422 | return err 423 | } 424 | 425 | beginIndex := paginator.Offset() 426 | endIndex := beginIndex + paginator.PerPageNums 427 | if int64(beginIndex) > paginator.Nums() { 428 | beginIndex = int(paginator.Nums()) 429 | } 430 | if int64(endIndex) > paginator.Nums() { 431 | endIndex = int(paginator.Nums()) 432 | } 433 | 434 | filteredDevices := devices[beginIndex:endIndex] 435 | result := make([]models.Device, len(filteredDevices)) 436 | for i, v := range filteredDevices { 437 | device := models.NewDevice(v) 438 | if err := applyNetworks(&device); err != nil { 439 | ctx.Logger().Errorf("failed to get networks for interface %s: %s", device.Name, err) 440 | return ctx.JSON(http.StatusInternalServerError, models.Error{ 441 | Code: "wireguard_device_error", 442 | Message: err.Error(), 443 | }) 444 | } 445 | 446 | result[i] = device 447 | } 448 | 449 | paginator.Write(ctx.Response()) 450 | return ctx.JSON(http.StatusOK, result) 451 | } 452 | 453 | // UpdateDevice - Update device 454 | func (c *WireGuardContainer) UpdateDevice(ctx echo.Context) error { 455 | name := ctx.Param("name") 456 | 457 | var request models.DeviceCreateOrUpdateRequest 458 | if err := ctx.Bind(&request); err != nil { 459 | return err 460 | } 461 | 462 | client, err := wgctrl.New() 463 | if err != nil { 464 | ctx.Logger().Errorf("failed to init wireguard ipc: %s", err) 465 | return ctx.JSON(http.StatusInternalServerError, models.Error{ 466 | Code: "wireguard_client_error", 467 | Message: err.Error(), 468 | }) 469 | } 470 | defer client.Close() 471 | 472 | _, err = client.Device(name) 473 | if err != nil { 474 | if os.IsNotExist(err) { 475 | return ctx.NoContent(http.StatusNotFound) 476 | } 477 | 478 | ctx.Logger().Errorf("failed to get wireguard device: %s", err) 479 | return ctx.JSON(http.StatusInternalServerError, models.Error{ 480 | Code: "wireguard_device_error", 481 | Message: err.Error(), 482 | }) 483 | } 484 | conf := wgtypes.Config{} 485 | err = request.Apply(&conf) 486 | if err != nil { 487 | ctx.Logger().Errorf("failed to get wireguard device conf: %s", err) 488 | return ctx.JSON(http.StatusInternalServerError, models.Error{ 489 | Code: "wireguard_config_error", 490 | Message: err.Error(), 491 | }) 492 | } 493 | 494 | if err := client.ConfigureDevice(name, conf); err != nil { 495 | ctx.Logger().Errorf("failed to configure wireguard device: %s", err) 496 | return ctx.JSON(http.StatusInternalServerError, models.Error{ 497 | Code: "wireguard_error", 498 | Message: err.Error(), 499 | }) 500 | } 501 | 502 | device, err := client.Device(name) 503 | if err != nil { 504 | if os.IsNotExist(err) { 505 | return ctx.NoContent(http.StatusNotFound) 506 | } 507 | 508 | ctx.Logger().Errorf("failed to get wireguard device: %s", err) 509 | return ctx.JSON(http.StatusInternalServerError, models.Error{ 510 | Code: "wireguard_device_error", 511 | Message: err.Error(), 512 | }) 513 | } 514 | 515 | result := models.NewDevice(device) 516 | if err := applyNetworks(&result); err != nil { 517 | ctx.Logger().Errorf("failed to get networks for interface %s: %s", result.Name, err) 518 | return ctx.JSON(http.StatusInternalServerError, models.Error{ 519 | Code: "wireguard_device_error", 520 | Message: err.Error(), 521 | }) 522 | } 523 | 524 | return ctx.JSON(http.StatusOK, result) 525 | } 526 | 527 | // UpdateDevicePeer - Update device's peer 528 | func (c *WireGuardContainer) UpdateDevicePeer(ctx echo.Context) error { 529 | name := ctx.Param("name") 530 | urlSafePubKey, err := url.QueryUnescape(ctx.Param("urlSafePubKey")) 531 | if err != nil { 532 | ctx.Logger().Errorf("failed to parse pub key: %s", err) 533 | return ctx.JSON(http.StatusBadRequest, models.Error{ 534 | Code: "request_params_error", 535 | Message: err.Error(), 536 | }) 537 | } 538 | pubKey, err := parseUrlSafeKey(urlSafePubKey) 539 | if err != nil { 540 | ctx.Logger().Errorf("failed to parse pub key: %s", err) 541 | return ctx.JSON(http.StatusBadRequest, models.Error{ 542 | Code: "request_params_error", 543 | Message: err.Error(), 544 | }) 545 | } 546 | 547 | var request models.PeerCreateOrUpdateRequest 548 | if err := ctx.Bind(&request); err != nil { 549 | return err 550 | } 551 | 552 | client, err := wgctrl.New() 553 | if err != nil { 554 | ctx.Logger().Errorf("failed to init wireguard ipc: %s", err) 555 | return ctx.JSON(http.StatusInternalServerError, models.Error{ 556 | Code: "wireguard_client_error", 557 | Message: err.Error(), 558 | }) 559 | } 560 | defer client.Close() 561 | 562 | _, err = client.Device(name) 563 | if err != nil { 564 | if os.IsNotExist(err) { 565 | return ctx.NoContent(http.StatusNotFound) 566 | } 567 | 568 | ctx.Logger().Errorf("failed to get wireguard device: %s", err) 569 | return ctx.JSON(http.StatusInternalServerError, models.Error{ 570 | Code: "wireguard_device_error", 571 | Message: err.Error(), 572 | }) 573 | } 574 | 575 | peerConf := wgtypes.PeerConfig{ 576 | PublicKey: pubKey, 577 | ReplaceAllowedIPs: true, 578 | UpdateOnly: true, 579 | } 580 | err = request.Apply(&peerConf) 581 | if err != nil { 582 | ctx.Logger().Errorf("failed to apply peer conf: %s", err) 583 | return ctx.JSON(http.StatusInternalServerError, models.Error{ 584 | Code: "wireguard_config_error", 585 | Message: err.Error(), 586 | }) 587 | } 588 | 589 | conf := wgtypes.Config{ 590 | Peers: []wgtypes.PeerConfig{ 591 | peerConf, 592 | }, 593 | } 594 | 595 | if err := client.ConfigureDevice(name, conf); err != nil { 596 | ctx.Logger().Errorf("failed to configure wireguard device: %s", err) 597 | return ctx.JSON(http.StatusInternalServerError, models.Error{ 598 | Code: "wireguard_error", 599 | Message: err.Error(), 600 | }) 601 | } 602 | 603 | if request.PrivateKey != nil { 604 | // store private key 605 | err := c.storage.WritePeerOptions(peerConf.PublicKey, storage.StorePeerOptions{ 606 | PrivateKey: *request.PrivateKey, 607 | }) 608 | 609 | if err != nil { 610 | ctx.Logger().Errorf("failed to save peer's options: %s", err) 611 | return ctx.JSON(http.StatusInternalServerError, models.Error{ 612 | Code: "wireguard_peer_error", 613 | Message: err.Error(), 614 | }) 615 | } 616 | } 617 | 618 | device, err := client.Device(name) 619 | if err != nil { 620 | if os.IsNotExist(err) { 621 | return ctx.NoContent(http.StatusNotFound) 622 | } 623 | 624 | ctx.Logger().Errorf("failed to get wireguard device: %s", err) 625 | return ctx.JSON(http.StatusInternalServerError, models.Error{ 626 | Code: "wireguard_device_error", 627 | Message: err.Error(), 628 | }) 629 | } 630 | 631 | var peer *wgtypes.Peer 632 | for _, v := range device.Peers { 633 | if v.PublicKey == pubKey { 634 | peer = &v 635 | break 636 | } 637 | } 638 | 639 | if peer == nil { 640 | return ctx.NoContent(http.StatusNotFound) 641 | } 642 | 643 | return ctx.JSON(http.StatusOK, models.NewPeer(*peer)) 644 | } 645 | 646 | func (c *WireGuardContainer) getDevicePeerQuickConfig(ctx echo.Context) (io.Reader, error) { 647 | name := ctx.Param("name") 648 | urlSafePubKey, err := url.QueryUnescape(ctx.Param("urlSafePubKey")) 649 | if err != nil { 650 | return nil, err 651 | } 652 | 653 | pubKey, err := parseUrlSafeKey(urlSafePubKey) 654 | if err != nil { 655 | return nil, err 656 | } 657 | 658 | peerOptions, err := c.storage.ReadPeerOptions(pubKey) 659 | if err != nil { 660 | return nil, err 661 | } 662 | 663 | deviceOptions, err := c.storage.ReadDeviceOptions(name) 664 | if err != nil && !os.IsNotExist(err) { 665 | return nil, err 666 | } 667 | 668 | if deviceOptions == nil { 669 | deviceOptions = &c.defaultDeviceOptions 670 | } 671 | 672 | client, err := wgctrl.New() 673 | if err != nil { 674 | return nil, err 675 | } 676 | defer client.Close() 677 | 678 | device, err := client.Device(name) 679 | if err != nil { 680 | return nil, err 681 | } 682 | 683 | var peer *wgtypes.Peer 684 | for _, v := range device.Peers { 685 | if v.PublicKey == pubKey { 686 | peer = &v 687 | break 688 | } 689 | } 690 | 691 | if peer == nil { 692 | return nil, os.ErrNotExist 693 | } 694 | 695 | quickConf, err := utils.GetPeerQuickConfig(*device, *peer, utils.PeerQuickConfigOptions{ 696 | PrivateKey: &peerOptions.PrivateKey, 697 | DNSServers: &deviceOptions.DNSServers, 698 | AllowedIPs: &deviceOptions.AllowedIPs, 699 | Host: &deviceOptions.Host, 700 | }) 701 | 702 | if err != nil { 703 | return nil, err 704 | } 705 | 706 | return quickConf, nil 707 | } 708 | 709 | // GetDevicePeerQuickConfig - Get device peer quick config 710 | func (c *WireGuardContainer) GetDevicePeerQuickConfig(ctx echo.Context) error { 711 | quickConf, err := c.getDevicePeerQuickConfig(ctx) 712 | if err != nil { 713 | ctx.Logger().Errorf("failed to get quick config: %s", err) 714 | return ctx.JSON(http.StatusBadRequest, models.Error{ 715 | Code: "request_params_error", 716 | Message: err.Error(), 717 | }) 718 | } 719 | 720 | return ctx.Stream(http.StatusOK, "text/plain", quickConf) 721 | } 722 | 723 | // GetDevicePeerQuickConfigQRCodePNG - Get device peer quick config QR code 724 | func (c *WireGuardContainer) GetDevicePeerQuickConfigQRCodePNG(ctx echo.Context) error { 725 | quickConf, err := c.getDevicePeerQuickConfig(ctx) 726 | if err != nil { 727 | ctx.Logger().Errorf("failed to get quick config: %s", err) 728 | return ctx.JSON(http.StatusBadRequest, models.Error{ 729 | Code: "request_params_error", 730 | Message: err.Error(), 731 | }) 732 | } 733 | 734 | widthParam := ctx.QueryParam("width") 735 | if widthParam == "" { 736 | widthParam = "256" 737 | } 738 | width, err := strconv.Atoi(widthParam) 739 | if err != nil { 740 | ctx.Logger().Errorf("failed to parse width: %s", err) 741 | return ctx.JSON(http.StatusBadRequest, models.Error{ 742 | Code: "request_params_error", 743 | Message: err.Error(), 744 | }) 745 | } 746 | 747 | buf := new(bytes.Buffer) 748 | if _, err := buf.ReadFrom(quickConf); err != nil { 749 | ctx.Logger().Errorf("failed to reade quick config: %s", err) 750 | return ctx.JSON(http.StatusBadRequest, models.Error{ 751 | Code: "request_params_error", 752 | Message: err.Error(), 753 | }) 754 | } 755 | 756 | qrBytes, err := qrcode.Encode(buf.String(), qrcode.Medium, width) 757 | if err != nil { 758 | ctx.Logger().Errorf("failed to generate qr code: %s", err) 759 | return ctx.JSON(http.StatusBadRequest, models.Error{ 760 | Code: "request_params_error", 761 | Message: err.Error(), 762 | }) 763 | } 764 | 765 | qrBuff := bytes.NewBuffer(qrBytes) 766 | return ctx.Stream(http.StatusOK, "image/png", qrBuff) 767 | } 768 | 769 | // GetDeviceOptions - Get device options 770 | func (c *WireGuardContainer) GetDeviceOptions(ctx echo.Context) error { 771 | options, err := c.storage.ReadDeviceOptions(ctx.Param("name")) 772 | if err != nil && !os.IsNotExist(err) { 773 | ctx.Logger().Errorf("failed to get device options: %s", err) 774 | return ctx.JSON(http.StatusInternalServerError, models.Error{ 775 | Code: "wireguard_device_error", 776 | Message: err.Error(), 777 | }) 778 | } 779 | 780 | if options == nil { 781 | options = &c.defaultDeviceOptions 782 | } 783 | 784 | return ctx.JSON(http.StatusOK, models.NewDeviceOptions(*options)) 785 | } 786 | 787 | // UpdateDeviceOptions - Update device's options 788 | func (c *WireGuardContainer) UpdateDeviceOptions(ctx echo.Context) error { 789 | var request models.DeviceOptionsUpdateRequest 790 | if err := ctx.Bind(&request); err != nil { 791 | return err 792 | } 793 | 794 | options, err := c.storage.ReadDeviceOptions(ctx.Param("name")) 795 | if err != nil && !os.IsNotExist(err) { 796 | ctx.Logger().Errorf("failed to get device options: %s", err) 797 | } 798 | 799 | if options == nil { 800 | options = &storage.StoreDeviceOptions{} 801 | } 802 | 803 | ctx.Logger().Printf("request: %+v\n", request) 804 | ctx.Logger().Printf("options: %+v\n", *options) 805 | 806 | if err := request.Apply(options); err != nil { 807 | ctx.Logger().Errorf("failed to update device options: %s", err) 808 | return ctx.JSON(http.StatusInternalServerError, models.Error{ 809 | Code: "wireguard_device_error", 810 | Message: err.Error(), 811 | }) 812 | } 813 | 814 | err = c.storage.WriteDeviceOptions(ctx.Param("name"), *options) 815 | if err != nil { 816 | ctx.Logger().Errorf("failed to save device options: %s", err) 817 | return ctx.JSON(http.StatusInternalServerError, models.Error{ 818 | Code: "wireguard_device_error", 819 | Message: err.Error(), 820 | }) 821 | } 822 | 823 | return ctx.JSON(http.StatusOK, models.NewDeviceOptions(*options)) 824 | } 825 | -------------------------------------------------------------------------------- /openapi-spec.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.3" 2 | info: 3 | title: WireGuard RESTful API 4 | description: | 5 | Manage WireGuard VPN tunnels by RESTful manner. 6 | 7 | Supported features: 8 | 9 | * Manage device: create, update, and delete wireguard interface 10 | * Manage device's ip addresses: attache or detach ip addresses to the netowrk interface 11 | * Manage device's peers: create, update, and delete peers 12 | * Peer's QR code, for use in WireGuard & ForestVPN client 13 | 14 | ForestVPN client may be used as alternative client with enabled P2P technology over WireGuard tunnelling. 15 | Read more on https://forestvpn.com/ 16 | termsOfService: https://forestvpn.com/terms/ 17 | contact: 18 | name: ForestVPN 19 | url: https://forestvpn.com/ 20 | email: support@forestvpn.com 21 | license: 22 | name: MIT 23 | url: https://opensource.org/licenses/MIT 24 | version: "1.0" 25 | 26 | externalDocs: 27 | description: Documentation of wgrest 28 | url: https://forestvpn.com/docs/wgrest/ 29 | 30 | servers: 31 | - url: '{scheme}://{host}/v1' 32 | variables: 33 | host: 34 | default: "example.com" 35 | scheme: 36 | enum: 37 | - 'https' 38 | - 'http' 39 | default: 'https' 40 | 41 | security: 42 | - bearerAuth: [ ] 43 | 44 | paths: 45 | /devices/: 46 | get: 47 | summary: Devices list 48 | operationId: ListDevices 49 | tags: 50 | - device 51 | parameters: 52 | - in: query 53 | name: per_page 54 | description: Number of WireGuard devices per page. Default is 100 55 | schema: 56 | type: number 57 | - in: query 58 | name: page 59 | description: Page number. Default is 0 60 | schema: 61 | type: number 62 | responses: 63 | '200': 64 | description: ok 65 | headers: 66 | Link: 67 | schema: 68 | type: string 69 | description: https://docs.github.com/en/rest/guides/traversing-with-pagination 70 | content: 71 | application/json: 72 | schema: 73 | type: array 74 | items: 75 | $ref: "#/components/schemas/Device" 76 | default: 77 | description: unexpected error 78 | content: 79 | application/json: 80 | schema: 81 | $ref: "#/components/schemas/Error" 82 | post: 83 | summary: Create new device 84 | operationId: CreateDevice 85 | tags: 86 | - device 87 | requestBody: 88 | content: 89 | application/json: 90 | schema: 91 | $ref: "#/components/schemas/DeviceCreateOrUpdateRequest" 92 | responses: 93 | 201: 94 | description: Ok 95 | content: 96 | application/json: 97 | schema: 98 | $ref: "#/components/schemas/Device" 99 | 409: 100 | description: Device exists 101 | content: 102 | application/json: 103 | schema: 104 | $ref: "#/components/schemas/Error" 105 | default: 106 | description: unexpected error 107 | content: 108 | application/json: 109 | schema: 110 | $ref: "#/components/schemas/Error" 111 | /devices/{name}/: 112 | get: 113 | summary: Get device info 114 | operationId: GetDevice 115 | tags: 116 | - device 117 | parameters: 118 | - in: path 119 | name: name 120 | required: true 121 | schema: 122 | type: string 123 | responses: 124 | '200': 125 | description: ok 126 | content: 127 | application/json: 128 | schema: 129 | $ref: "#/components/schemas/Device" 130 | 404: 131 | description: not found 132 | default: 133 | description: unexpected error 134 | content: 135 | application/json: 136 | schema: 137 | $ref: "#/components/schemas/Error" 138 | delete: 139 | summary: Delete Device 140 | operationId: DeleteDevice 141 | tags: 142 | - device 143 | parameters: 144 | - in: path 145 | name: name 146 | required: true 147 | schema: 148 | type: string 149 | responses: 150 | 204: 151 | description: No content 152 | 404: 153 | description: Not found 154 | default: 155 | description: Unexpected error 156 | content: 157 | application/json: 158 | schema: 159 | $ref: "#/components/schemas/Error" 160 | patch: 161 | summary: Update device 162 | operationId: UpdateDevice 163 | tags: 164 | - device 165 | parameters: 166 | - in: path 167 | name: name 168 | required: true 169 | schema: 170 | type: string 171 | requestBody: 172 | required: true 173 | content: 174 | application/json: 175 | schema: 176 | $ref: "#/components/schemas/DeviceCreateOrUpdateRequest" 177 | responses: 178 | 200: 179 | description: Ok 180 | content: 181 | application/json: 182 | schema: 183 | $ref: "#/components/schemas/Device" 184 | 404: 185 | description: Not found 186 | default: 187 | description: unexpected error 188 | content: 189 | application/json: 190 | schema: 191 | $ref: "#/components/schemas/Error" 192 | /devices/{name}/options/: 193 | get: 194 | summary: Get device options 195 | operationId: GetDeviceOptions 196 | tags: 197 | - device 198 | parameters: 199 | - in: path 200 | name: name 201 | required: true 202 | schema: 203 | type: string 204 | responses: 205 | '200': 206 | description: ok 207 | content: 208 | application/json: 209 | schema: 210 | $ref: "#/components/schemas/DeviceOptions" 211 | 404: 212 | description: not found 213 | default: 214 | description: unexpected error 215 | content: 216 | application/json: 217 | schema: 218 | $ref: "#/components/schemas/Error" 219 | patch: 220 | summary: Update device's options 221 | operationId: UpdateDeviceOptions 222 | tags: 223 | - device 224 | parameters: 225 | - in: path 226 | name: name 227 | required: true 228 | schema: 229 | type: string 230 | requestBody: 231 | required: true 232 | content: 233 | application/json: 234 | schema: 235 | $ref: "#/components/schemas/DeviceOptionsUpdateRequest" 236 | responses: 237 | 200: 238 | description: Ok 239 | content: 240 | application/json: 241 | schema: 242 | $ref: "#/components/schemas/DeviceOptions" 243 | 404: 244 | description: Not found 245 | default: 246 | description: unexpected error 247 | content: 248 | application/json: 249 | schema: 250 | $ref: "#/components/schemas/Error" 251 | /devices/{name}/peers/: 252 | get: 253 | summary: Peers list 254 | operationId: ListDevicePeers 255 | tags: 256 | - device 257 | parameters: 258 | - in: path 259 | name: name 260 | description: Device's name 261 | required: true 262 | schema: 263 | type: string 264 | - in: query 265 | name: per_page 266 | description: Number of WireGuard device's peers per page. Default is 100 267 | schema: 268 | type: number 269 | - in: query 270 | name: page 271 | description: Page number. Default is 0 272 | schema: 273 | type: number 274 | - in: query 275 | name: q 276 | description: Search query 277 | schema: 278 | type: string 279 | - in: query 280 | name: sort 281 | description: Sort field 282 | schema: 283 | type: string 284 | enum: 285 | - pub_key 286 | - receive_bytes 287 | - -receive_bytes 288 | - transmit_bytes 289 | - -transmit_bytes 290 | - total_bytes 291 | - -total_bytes 292 | - last_handshake_time 293 | - -last_handshake_time 294 | responses: 295 | 200: 296 | description: Ok 297 | headers: 298 | Link: 299 | schema: 300 | type: string 301 | description: https://docs.github.com/en/rest/guides/traversing-with-pagination 302 | content: 303 | application/json: 304 | schema: 305 | type: array 306 | items: 307 | $ref: "#/components/schemas/Peer" 308 | 404: 309 | description: Device not found 310 | default: 311 | description: Unexpected error 312 | content: 313 | application/json: 314 | schema: 315 | $ref: "#/components/schemas/Error" 316 | post: 317 | summary: Create new device peer 318 | operationId: CreateDevicePeer 319 | tags: 320 | - device 321 | parameters: 322 | - in: path 323 | name: name 324 | description: Device's name 325 | required: true 326 | schema: 327 | type: string 328 | requestBody: 329 | content: 330 | application/json: 331 | schema: 332 | $ref: "#/components/schemas/PeerCreateOrUpdateRequest" 333 | responses: 334 | 201: 335 | description: Created 336 | content: 337 | application/json: 338 | schema: 339 | $ref: "#/components/schemas/Peer" 340 | 404: 341 | description: Device not found 342 | default: 343 | description: Unexpected error 344 | content: 345 | application/json: 346 | schema: 347 | $ref: "#/components/schemas/Error" 348 | /devices/{name}/peers/{urlSafePubKey}/: 349 | get: 350 | summary: Get device peer info 351 | operationId: GetDevicePeer 352 | tags: 353 | - device 354 | parameters: 355 | - in: path 356 | name: name 357 | description: Device's name 358 | required: true 359 | schema: 360 | type: string 361 | - in: path 362 | name: urlSafePubKey 363 | description: Peer's url safe public key 364 | required: true 365 | schema: 366 | type: string 367 | responses: 368 | 200: 369 | description: Ok 370 | content: 371 | application/json: 372 | schema: 373 | $ref: "#/components/schemas/Peer" 374 | 404: 375 | description: Peer or device not found 376 | default: 377 | description: Unexpected error 378 | content: 379 | application/json: 380 | schema: 381 | $ref: "#/components/schemas/Error" 382 | patch: 383 | summary: Update device's peer 384 | operationId: UpdateDevicePeer 385 | tags: 386 | - device 387 | parameters: 388 | - in: path 389 | name: name 390 | description: Device's name 391 | required: true 392 | schema: 393 | type: string 394 | - in: path 395 | name: urlSafePubKey 396 | description: Peer's url safe public key 397 | required: true 398 | schema: 399 | type: string 400 | requestBody: 401 | content: 402 | application/json: 403 | schema: 404 | $ref: "#/components/schemas/PeerCreateOrUpdateRequest" 405 | responses: 406 | 200: 407 | description: Ok 408 | content: 409 | application/json: 410 | schema: 411 | $ref: "#/components/schemas/Peer" 412 | 404: 413 | description: Peer or device not found 414 | default: 415 | description: Unexpected error 416 | content: 417 | application/json: 418 | schema: 419 | $ref: "#/components/schemas/Error" 420 | delete: 421 | summary: Delete device's peer 422 | operationId: DeleteDevicePeer 423 | tags: 424 | - device 425 | parameters: 426 | - in: path 427 | name: name 428 | description: Device's name 429 | required: true 430 | schema: 431 | type: string 432 | - in: path 433 | name: urlSafePubKey 434 | description: Peer's url safe public key 435 | required: true 436 | schema: 437 | type: string 438 | responses: 439 | 200: 440 | description: Ok 441 | content: 442 | application/json: 443 | schema: 444 | $ref: "#/components/schemas/Peer" 445 | 404: 446 | description: Peer or device not found 447 | default: 448 | description: Unexpected error 449 | content: 450 | application/json: 451 | schema: 452 | $ref: "#/components/schemas/Error" 453 | /devices/{name}/peers/{urlSafePubKey}/quick.conf: 454 | get: 455 | summary: Get device peer quick config 456 | operationId: GetDevicePeerQuickConfig 457 | tags: 458 | - device 459 | parameters: 460 | - in: path 461 | name: name 462 | description: Device's name 463 | required: true 464 | schema: 465 | type: string 466 | - in: path 467 | name: urlSafePubKey 468 | description: Peer's url safe public key 469 | required: true 470 | schema: 471 | type: string 472 | responses: 473 | 200: 474 | description: Ok 475 | content: 476 | text/plain: 477 | schema: 478 | type: string 479 | format: binary 480 | 404: 481 | description: Peer or device not found 482 | default: 483 | description: Unexpected error 484 | content: 485 | application/json: 486 | schema: 487 | $ref: "#/components/schemas/Error" 488 | /devices/{name}/peers/{urlSafePubKey}/quick.conf.png: 489 | get: 490 | summary: Get device peer quick config QR code 491 | operationId: GetDevicePeerQuickConfigQRCodePNG 492 | tags: 493 | - device 494 | parameters: 495 | - in: path 496 | name: name 497 | description: Device's name 498 | required: true 499 | schema: 500 | type: string 501 | - in: path 502 | name: urlSafePubKey 503 | description: Peer's url safe public key 504 | required: true 505 | schema: 506 | type: string 507 | - in: query 508 | name: width 509 | description: QR code's width & height. Default is 256px. 510 | schema: 511 | type: string 512 | responses: 513 | 200: 514 | description: Ok 515 | content: 516 | image/jpeg: 517 | schema: 518 | type: string 519 | format: binary 520 | 404: 521 | description: Peer or device not found 522 | default: 523 | description: Unexpected error 524 | content: 525 | application/json: 526 | schema: 527 | $ref: "#/components/schemas/Error" 528 | components: 529 | securitySchemes: 530 | bearerAuth: 531 | type: http 532 | scheme: bearer 533 | description: Authorization token 534 | schemas: 535 | Error: 536 | type: object 537 | required: 538 | - code 539 | - message 540 | properties: 541 | code: 542 | type: string 543 | description: Error code 544 | example: device_does_not_exist 545 | message: 546 | type: string 547 | description: Error's short description 548 | example: Device "wg0" does'n exist 549 | detail: 550 | type: string 551 | description: Error's detail description 552 | DeviceOptionsUpdateRequest: 553 | type: object 554 | description: Device options 555 | properties: 556 | allowed_ips: 557 | type: array 558 | items: 559 | type: string 560 | description: Device's allowed ips, it might be any of IPv4 or IPv6 addresses in CIDR notation. 561 | It might be owervrite in peer and device config. 562 | example: 563 | - 0.0.0.0/0 564 | - ::0/0 565 | nullable: true 566 | dns_servers: 567 | type: array 568 | items: 569 | type: string 570 | description: Interface's DNS servers. 571 | example: 572 | - 8.8.8.8 573 | - 2001:4860:4860::8888 574 | nullable: true 575 | host: 576 | type: string 577 | description: Device host, it might be domain name or IPv4/IPv6 address. 578 | It is used for external/internal connection 579 | example: "1.2.3.4" 580 | nullable: true 581 | DeviceOptions: 582 | type: object 583 | description: Device options 584 | required: 585 | - allowed_ips 586 | - dns_servers 587 | - host 588 | properties: 589 | allowed_ips: 590 | type: array 591 | items: 592 | type: string 593 | description: Device's allowed ips, it might be any of IPv4 or IPv6 addresses in CIDR notation. 594 | It might be owervrite in peer and device config. 595 | example: 596 | - 0.0.0.0/0 597 | - ::0/0 598 | dns_servers: 599 | type: array 600 | items: 601 | type: string 602 | description: Interface's DNS servers. 603 | example: 604 | - 8.8.8.8 605 | - 2001:4860:4860::8888 606 | host: 607 | type: string 608 | description: Device host, it might be domain name or IPv4/IPv6 address. 609 | It is used for external/internal connection 610 | example: "1.2.3.4" 611 | DeviceCreateOrUpdateRequest: 612 | type: object 613 | description: Device params that might be used due to creation or updation process 614 | properties: 615 | name: 616 | type: string 617 | description: WireGuard device name. Usually it is network interface name 618 | example: wg0 619 | nullable: true 620 | listen_port: 621 | type: integer 622 | format: int32 623 | description: WireGuard device listen port. 624 | example: 51820 625 | nullable: true 626 | private_key: 627 | type: string 628 | description: WireGuard device private key encoded by base64. 629 | example: wBHGU3RiK/IFWXAF2jbHjGSDAKEO2ddcsZFEWcQ+qGc= 630 | nullable: true 631 | firewall_mark: 632 | type: integer 633 | format: int32 634 | description: WireGuard device firewall mark. 635 | example: 10 636 | nullable: true 637 | networks: 638 | type: array 639 | items: 640 | type: string 641 | description: IPv4 or IPv6 addresses in CIDR notation 642 | example: 643 | - 10.71.25.1/24 644 | - fd42:21:21::1/64 645 | nullable: true 646 | Device: 647 | type: object 648 | description: Information about wireguard device. 649 | required: 650 | - name 651 | - listen_port 652 | - public_key 653 | - private_key 654 | - firewall_mark 655 | - networks 656 | - peers_count 657 | - total_receive_bytes 658 | - total_transmit_bytes 659 | properties: 660 | name: 661 | type: string 662 | description: WireGuard device name. Usually it is network interface name 663 | example: wg0 664 | listen_port: 665 | type: integer 666 | format: int32 667 | description: WireGuard device listen port. 668 | example: 51820 669 | public_key: 670 | type: string 671 | description: WireGuard device public key encoded by base64. 672 | example: QFjZjxa2sgwnmGT4NqyRoeNk31AlHjVxHNEH/qY/2no= 673 | firewall_mark: 674 | type: integer 675 | format: int32 676 | description: WireGuard device firewall mark. 677 | example: 10 678 | networks: 679 | type: array 680 | items: 681 | type: string 682 | description: IPv4 or IPv6 addresses in CIDR notation 683 | example: 684 | - 10.71.25.1/24 685 | - fd42:21:21::1/64 686 | peers_count: 687 | type: integer 688 | format: int32 689 | description: WireGuard device's peers count 690 | example: 10 691 | total_receive_bytes: 692 | type: integer 693 | format: int64 694 | description: WireGuard device's peers total receive bytes 695 | example: 59984733 696 | total_transmit_bytes: 697 | type: integer 698 | format: int64 699 | description: WireGuard device's peers total transmit bytes 700 | example: 45331987 701 | PeerCreateOrUpdateRequest: 702 | type: object 703 | description: Peer params that might be used due to creation or updation process 704 | properties: 705 | private_key: 706 | type: string 707 | description: Base64 encoded private key. If present it will be stored in persistent storage. 708 | example: gD89VQVXPAEpXuIyHOujw1wF4njIBtaSLvqAatBAuWY= 709 | nullable: true 710 | public_key: 711 | type: string 712 | description: Base64 encoded public key 713 | example: qnPJFozrAnrTjap5VjO30bUeLvhkZEEZx48w2RqMpRA= 714 | nullable: true 715 | preshared_key: 716 | type: string 717 | description: Base64 encoded preshared key 718 | example: c2m+JtxpcRP6pztdDFRHnOx75SI+QyBDba1+BEbQaiA= 719 | nullable: true 720 | allowed_ips: 721 | type: array 722 | items: 723 | type: string 724 | description: Peer's allowed ips, it might be any of IPv4 or IPv6 addresses in CIDR notation 725 | example: 726 | - 10.71.25.51/32 727 | - fd42:21:21::51/128 728 | nullable: true 729 | persistent_keepalive_interval: 730 | type: string 731 | description: Peer's persistend keepalive interval. Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". 732 | example: "25s" 733 | endpoint: 734 | type: string 735 | description: Peer's endpoint in host:port format 736 | example: "1.2.3.4:2345" 737 | Peer: 738 | type: object 739 | description: Information about wireguard peer. 740 | required: 741 | - public_key 742 | - url_safe_public_key 743 | - allowed_ips 744 | - persistent_keepalive_interval 745 | - last_handshake_time 746 | - endpoint 747 | - receive_bytes 748 | - transmit_bytes 749 | properties: 750 | public_key: 751 | type: string 752 | description: Base64 encoded public key 753 | example: 0DGpyohLU+T1qAemWVWsNd1nwy3ZBAG7U4JJ/ZA+fWA= 754 | url_safe_public_key: 755 | type: string 756 | description: URL safe base64 encoded public key. It is usefull to use in peers api endpoint. 757 | example: 0DGpyohLU-T1qAemWVWsNd1nwy3ZBAG7U4JJ_ZA-fWA 758 | preshared_key: 759 | type: string 760 | description: Base64 encoded preshared key 761 | example: c2m+JtxpcRP6pztdDFRHnOx75SI+QyBDba1+BEbQaiA= 762 | allowed_ips: 763 | type: array 764 | items: 765 | type: string 766 | description: Peer's allowed ips, it might be any of IPv4 or IPv6 addresses in CIDR notation 767 | example: 768 | - 10.71.25.51/32 769 | - fd42:21:21::51/128 770 | last_handshake_time: 771 | type: string 772 | format: date-time 773 | description: Peer's last handshake time formated in RFC3339 774 | example: "" 775 | persistent_keepalive_interval: 776 | type: string 777 | description: Peer's persistend keepalive interval in 778 | example: "25s" 779 | endpoint: 780 | type: string 781 | description: Peer's endpoint in host:port format 782 | example: "1.2.3.4:2345" 783 | receive_bytes: 784 | type: integer 785 | format: int64 786 | description: Peer's receive bytes 787 | example: 587732 788 | transmit_bytes: 789 | type: integer 790 | format: int64 791 | description: Peer's transmit bytes 792 | example: 432445 793 | -------------------------------------------------------------------------------- /.docs/api/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | contact: 4 | email: support@forestvpn.com 5 | name: ForestVPN 6 | url: https://forestvpn.com/ 7 | description: | 8 | Manage WireGuard VPN tunnels by RESTful manner. 9 | 10 | Supported features: 11 | 12 | * Manage device: create, update, and delete wireguard interface 13 | * Manage device's ip addresses: attache or detach ip addresses to the netowrk interface 14 | * Manage device's peers: create, update, and delete peers 15 | * Peer's QR code, for use in WireGuard & ForestVPN client 16 | 17 | ForestVPN client may be used as alternative client with enabled P2P technology over WireGuard tunnelling. 18 | Read more on https://forestvpn.com/ 19 | license: 20 | name: MIT 21 | url: https://opensource.org/licenses/MIT 22 | termsOfService: https://forestvpn.com/terms/ 23 | title: WireGuard RESTful API 24 | version: "1.0" 25 | externalDocs: 26 | description: Documentation of wgrest 27 | url: https://forestvpn.com/docs/wgrest/ 28 | servers: 29 | - url: '{scheme}://{host}/v1' 30 | variables: 31 | host: 32 | default: example.com 33 | scheme: 34 | default: https 35 | enum: 36 | - https 37 | - http 38 | security: 39 | - bearerAuth: [] 40 | paths: 41 | /devices/: 42 | get: 43 | operationId: ListDevices 44 | parameters: 45 | - description: Number of WireGuard devices per page. Default is 100 46 | explode: true 47 | in: query 48 | name: per_page 49 | required: false 50 | schema: 51 | type: number 52 | style: form 53 | - description: Page number. Default is 0 54 | explode: true 55 | in: query 56 | name: page 57 | required: false 58 | schema: 59 | type: number 60 | style: form 61 | responses: 62 | "200": 63 | content: 64 | application/json: 65 | schema: 66 | items: 67 | $ref: '#/components/schemas/Device' 68 | type: array 69 | description: ok 70 | headers: 71 | Link: 72 | explode: false 73 | schema: 74 | description: https://docs.github.com/en/rest/guides/traversing-with-pagination 75 | type: string 76 | style: simple 77 | default: 78 | content: 79 | application/json: 80 | schema: 81 | $ref: '#/components/schemas/Error' 82 | description: unexpected error 83 | summary: Devices list 84 | tags: 85 | - device 86 | post: 87 | operationId: CreateDevice 88 | requestBody: 89 | content: 90 | application/json: 91 | schema: 92 | $ref: '#/components/schemas/DeviceCreateOrUpdateRequest' 93 | responses: 94 | "201": 95 | content: 96 | application/json: 97 | schema: 98 | $ref: '#/components/schemas/Device' 99 | description: Ok 100 | "409": 101 | content: 102 | application/json: 103 | schema: 104 | $ref: '#/components/schemas/Error' 105 | description: Device exists 106 | default: 107 | content: 108 | application/json: 109 | schema: 110 | $ref: '#/components/schemas/Error' 111 | description: unexpected error 112 | summary: Create new device 113 | tags: 114 | - device 115 | /devices/{name}/: 116 | delete: 117 | operationId: DeleteDevice 118 | parameters: 119 | - explode: false 120 | in: path 121 | name: name 122 | required: true 123 | schema: 124 | type: string 125 | style: simple 126 | responses: 127 | "204": 128 | description: No content 129 | "404": 130 | description: Not found 131 | default: 132 | content: 133 | application/json: 134 | schema: 135 | $ref: '#/components/schemas/Error' 136 | description: Unexpected error 137 | summary: Delete Device 138 | tags: 139 | - device 140 | get: 141 | operationId: GetDevice 142 | parameters: 143 | - explode: false 144 | in: path 145 | name: name 146 | required: true 147 | schema: 148 | type: string 149 | style: simple 150 | responses: 151 | "200": 152 | content: 153 | application/json: 154 | schema: 155 | $ref: '#/components/schemas/Device' 156 | description: ok 157 | "404": 158 | description: not found 159 | default: 160 | content: 161 | application/json: 162 | schema: 163 | $ref: '#/components/schemas/Error' 164 | description: unexpected error 165 | summary: Get device info 166 | tags: 167 | - device 168 | patch: 169 | operationId: UpdateDevice 170 | parameters: 171 | - explode: false 172 | in: path 173 | name: name 174 | required: true 175 | schema: 176 | type: string 177 | style: simple 178 | requestBody: 179 | content: 180 | application/json: 181 | schema: 182 | $ref: '#/components/schemas/DeviceCreateOrUpdateRequest' 183 | required: true 184 | responses: 185 | "200": 186 | content: 187 | application/json: 188 | schema: 189 | $ref: '#/components/schemas/Device' 190 | description: Ok 191 | "404": 192 | description: Not found 193 | default: 194 | content: 195 | application/json: 196 | schema: 197 | $ref: '#/components/schemas/Error' 198 | description: unexpected error 199 | summary: Update device 200 | tags: 201 | - device 202 | /devices/{name}/options/: 203 | get: 204 | operationId: GetDeviceOptions 205 | parameters: 206 | - explode: false 207 | in: path 208 | name: name 209 | required: true 210 | schema: 211 | type: string 212 | style: simple 213 | responses: 214 | "200": 215 | content: 216 | application/json: 217 | schema: 218 | $ref: '#/components/schemas/DeviceOptions' 219 | description: ok 220 | "404": 221 | description: not found 222 | default: 223 | content: 224 | application/json: 225 | schema: 226 | $ref: '#/components/schemas/Error' 227 | description: unexpected error 228 | summary: Get device options 229 | tags: 230 | - device 231 | patch: 232 | operationId: UpdateDeviceOptions 233 | parameters: 234 | - explode: false 235 | in: path 236 | name: name 237 | required: true 238 | schema: 239 | type: string 240 | style: simple 241 | requestBody: 242 | content: 243 | application/json: 244 | schema: 245 | $ref: '#/components/schemas/DeviceOptionsUpdateRequest' 246 | required: true 247 | responses: 248 | "200": 249 | content: 250 | application/json: 251 | schema: 252 | $ref: '#/components/schemas/DeviceOptions' 253 | description: Ok 254 | "404": 255 | description: Not found 256 | default: 257 | content: 258 | application/json: 259 | schema: 260 | $ref: '#/components/schemas/Error' 261 | description: unexpected error 262 | summary: Update device's options 263 | tags: 264 | - device 265 | /devices/{name}/peers/: 266 | get: 267 | operationId: ListDevicePeers 268 | parameters: 269 | - description: Device's name 270 | explode: false 271 | in: path 272 | name: name 273 | required: true 274 | schema: 275 | type: string 276 | style: simple 277 | - description: Number of WireGuard device's peers per page. Default is 100 278 | explode: true 279 | in: query 280 | name: per_page 281 | required: false 282 | schema: 283 | type: number 284 | style: form 285 | - description: Page number. Default is 0 286 | explode: true 287 | in: query 288 | name: page 289 | required: false 290 | schema: 291 | type: number 292 | style: form 293 | - description: Search query 294 | explode: true 295 | in: query 296 | name: q 297 | required: false 298 | schema: 299 | type: string 300 | style: form 301 | - description: Sort field 302 | explode: true 303 | in: query 304 | name: sort 305 | required: false 306 | schema: 307 | enum: 308 | - pub_key 309 | - receive_bytes 310 | - -receive_bytes 311 | - transmit_bytes 312 | - -transmit_bytes 313 | - total_bytes 314 | - -total_bytes 315 | - last_handshake_time 316 | - -last_handshake_time 317 | type: string 318 | style: form 319 | responses: 320 | "200": 321 | content: 322 | application/json: 323 | schema: 324 | items: 325 | $ref: '#/components/schemas/Peer' 326 | type: array 327 | description: Ok 328 | headers: 329 | Link: 330 | explode: false 331 | schema: 332 | description: https://docs.github.com/en/rest/guides/traversing-with-pagination 333 | type: string 334 | style: simple 335 | "404": 336 | description: Device not found 337 | default: 338 | content: 339 | application/json: 340 | schema: 341 | $ref: '#/components/schemas/Error' 342 | description: Unexpected error 343 | summary: Peers list 344 | tags: 345 | - device 346 | post: 347 | operationId: CreateDevicePeer 348 | parameters: 349 | - description: Device's name 350 | explode: false 351 | in: path 352 | name: name 353 | required: true 354 | schema: 355 | type: string 356 | style: simple 357 | requestBody: 358 | content: 359 | application/json: 360 | schema: 361 | $ref: '#/components/schemas/PeerCreateOrUpdateRequest' 362 | responses: 363 | "201": 364 | content: 365 | application/json: 366 | schema: 367 | $ref: '#/components/schemas/Peer' 368 | description: Created 369 | "404": 370 | description: Device not found 371 | default: 372 | content: 373 | application/json: 374 | schema: 375 | $ref: '#/components/schemas/Error' 376 | description: Unexpected error 377 | summary: Create new device peer 378 | tags: 379 | - device 380 | /devices/{name}/peers/{urlSafePubKey}/: 381 | delete: 382 | operationId: DeleteDevicePeer 383 | parameters: 384 | - description: Device's name 385 | explode: false 386 | in: path 387 | name: name 388 | required: true 389 | schema: 390 | type: string 391 | style: simple 392 | - description: Peer's url safe public key 393 | explode: false 394 | in: path 395 | name: urlSafePubKey 396 | required: true 397 | schema: 398 | type: string 399 | style: simple 400 | responses: 401 | "200": 402 | content: 403 | application/json: 404 | schema: 405 | $ref: '#/components/schemas/Peer' 406 | description: Ok 407 | "404": 408 | description: Peer or device not found 409 | default: 410 | content: 411 | application/json: 412 | schema: 413 | $ref: '#/components/schemas/Error' 414 | description: Unexpected error 415 | summary: Delete device's peer 416 | tags: 417 | - device 418 | get: 419 | operationId: GetDevicePeer 420 | parameters: 421 | - description: Device's name 422 | explode: false 423 | in: path 424 | name: name 425 | required: true 426 | schema: 427 | type: string 428 | style: simple 429 | - description: Peer's url safe public key 430 | explode: false 431 | in: path 432 | name: urlSafePubKey 433 | required: true 434 | schema: 435 | type: string 436 | style: simple 437 | responses: 438 | "200": 439 | content: 440 | application/json: 441 | schema: 442 | $ref: '#/components/schemas/Peer' 443 | description: Ok 444 | "404": 445 | description: Peer or device not found 446 | default: 447 | content: 448 | application/json: 449 | schema: 450 | $ref: '#/components/schemas/Error' 451 | description: Unexpected error 452 | summary: Get device peer info 453 | tags: 454 | - device 455 | patch: 456 | operationId: UpdateDevicePeer 457 | parameters: 458 | - description: Device's name 459 | explode: false 460 | in: path 461 | name: name 462 | required: true 463 | schema: 464 | type: string 465 | style: simple 466 | - description: Peer's url safe public key 467 | explode: false 468 | in: path 469 | name: urlSafePubKey 470 | required: true 471 | schema: 472 | type: string 473 | style: simple 474 | requestBody: 475 | content: 476 | application/json: 477 | schema: 478 | $ref: '#/components/schemas/PeerCreateOrUpdateRequest' 479 | responses: 480 | "200": 481 | content: 482 | application/json: 483 | schema: 484 | $ref: '#/components/schemas/Peer' 485 | description: Ok 486 | "404": 487 | description: Peer or device not found 488 | default: 489 | content: 490 | application/json: 491 | schema: 492 | $ref: '#/components/schemas/Error' 493 | description: Unexpected error 494 | summary: Update device's peer 495 | tags: 496 | - device 497 | /devices/{name}/peers/{urlSafePubKey}/quick.conf: 498 | get: 499 | operationId: GetDevicePeerQuickConfig 500 | parameters: 501 | - description: Device's name 502 | explode: false 503 | in: path 504 | name: name 505 | required: true 506 | schema: 507 | type: string 508 | style: simple 509 | - description: Peer's url safe public key 510 | explode: false 511 | in: path 512 | name: urlSafePubKey 513 | required: true 514 | schema: 515 | type: string 516 | style: simple 517 | responses: 518 | "200": 519 | content: 520 | text/plain: 521 | schema: 522 | format: binary 523 | type: string 524 | description: Ok 525 | "404": 526 | description: Peer or device not found 527 | default: 528 | content: 529 | application/json: 530 | schema: 531 | $ref: '#/components/schemas/Error' 532 | description: Unexpected error 533 | summary: Get device peer quick config 534 | tags: 535 | - device 536 | /devices/{name}/peers/{urlSafePubKey}/quick.conf.png: 537 | get: 538 | operationId: GetDevicePeerQuickConfigQRCodePNG 539 | parameters: 540 | - description: Device's name 541 | explode: false 542 | in: path 543 | name: name 544 | required: true 545 | schema: 546 | type: string 547 | style: simple 548 | - description: Peer's url safe public key 549 | explode: false 550 | in: path 551 | name: urlSafePubKey 552 | required: true 553 | schema: 554 | type: string 555 | style: simple 556 | - description: QR code's width & height. Default is 256px. 557 | explode: true 558 | in: query 559 | name: width 560 | required: false 561 | schema: 562 | type: string 563 | style: form 564 | responses: 565 | "200": 566 | content: 567 | image/jpeg: 568 | schema: 569 | format: binary 570 | type: string 571 | description: Ok 572 | "404": 573 | description: Peer or device not found 574 | default: 575 | content: 576 | application/json: 577 | schema: 578 | $ref: '#/components/schemas/Error' 579 | description: Unexpected error 580 | summary: Get device peer quick config QR code 581 | tags: 582 | - device 583 | components: 584 | schemas: 585 | Error: 586 | properties: 587 | code: 588 | description: Error code 589 | example: device_does_not_exist 590 | type: string 591 | message: 592 | description: Error's short description 593 | example: Device "wg0" does'n exist 594 | type: string 595 | detail: 596 | description: Error's detail description 597 | type: string 598 | required: 599 | - code 600 | - message 601 | type: object 602 | DeviceOptionsUpdateRequest: 603 | description: Device options 604 | example: 605 | allowed_ips: 606 | - 0.0.0.0/0 607 | - ::0/0 608 | host: 1.2.3.4 609 | dns_servers: 610 | - 8.8.8.8 611 | - 2001:4860:4860::8888 612 | properties: 613 | allowed_ips: 614 | description: Device's allowed ips, it might be any of IPv4 or IPv6 addresses 615 | in CIDR notation. It might be owervrite in peer and device config. 616 | example: 617 | - 0.0.0.0/0 618 | - ::0/0 619 | items: 620 | type: string 621 | nullable: true 622 | type: array 623 | dns_servers: 624 | description: Interface's DNS servers. 625 | example: 626 | - 8.8.8.8 627 | - 2001:4860:4860::8888 628 | items: 629 | type: string 630 | nullable: true 631 | type: array 632 | host: 633 | description: Device host, it might be domain name or IPv4/IPv6 address. 634 | It is used for external/internal connection 635 | example: 1.2.3.4 636 | nullable: true 637 | type: string 638 | type: object 639 | DeviceOptions: 640 | description: Device options 641 | example: 642 | allowed_ips: 643 | - 0.0.0.0/0 644 | - ::0/0 645 | host: 1.2.3.4 646 | dns_servers: 647 | - 8.8.8.8 648 | - 2001:4860:4860::8888 649 | properties: 650 | allowed_ips: 651 | description: Device's allowed ips, it might be any of IPv4 or IPv6 addresses 652 | in CIDR notation. It might be owervrite in peer and device config. 653 | example: 654 | - 0.0.0.0/0 655 | - ::0/0 656 | items: 657 | type: string 658 | type: array 659 | dns_servers: 660 | description: Interface's DNS servers. 661 | example: 662 | - 8.8.8.8 663 | - 2001:4860:4860::8888 664 | items: 665 | type: string 666 | type: array 667 | host: 668 | description: Device host, it might be domain name or IPv4/IPv6 address. 669 | It is used for external/internal connection 670 | example: 1.2.3.4 671 | type: string 672 | required: 673 | - allowed_ips 674 | - dns_servers 675 | - host 676 | type: object 677 | DeviceCreateOrUpdateRequest: 678 | description: Device params that might be used due to creation or updation process 679 | example: 680 | firewall_mark: 10 681 | listen_port: 51820 682 | name: wg0 683 | private_key: wBHGU3RiK/IFWXAF2jbHjGSDAKEO2ddcsZFEWcQ+qGc= 684 | networks: 685 | - 10.71.25.1/24 686 | - fd42:21:21::1/64 687 | properties: 688 | name: 689 | description: WireGuard device name. Usually it is network interface name 690 | example: wg0 691 | nullable: true 692 | type: string 693 | listen_port: 694 | description: WireGuard device listen port. 695 | example: 51820 696 | format: int32 697 | nullable: true 698 | type: integer 699 | private_key: 700 | description: WireGuard device private key encoded by base64. 701 | example: wBHGU3RiK/IFWXAF2jbHjGSDAKEO2ddcsZFEWcQ+qGc= 702 | nullable: true 703 | type: string 704 | firewall_mark: 705 | description: WireGuard device firewall mark. 706 | example: 10 707 | format: int32 708 | nullable: true 709 | type: integer 710 | networks: 711 | description: IPv4 or IPv6 addresses in CIDR notation 712 | example: 713 | - 10.71.25.1/24 714 | - fd42:21:21::1/64 715 | items: 716 | type: string 717 | nullable: true 718 | type: array 719 | type: object 720 | Device: 721 | description: Information about wireguard device. 722 | example: 723 | public_key: QFjZjxa2sgwnmGT4NqyRoeNk31AlHjVxHNEH/qY/2no= 724 | firewall_mark: 10 725 | listen_port: 51820 726 | total_receive_bytes: 59984733 727 | name: wg0 728 | networks: 729 | - 10.71.25.1/24 730 | - fd42:21:21::1/64 731 | peers_count: 10 732 | total_transmit_bytes: 45331987 733 | properties: 734 | name: 735 | description: WireGuard device name. Usually it is network interface name 736 | example: wg0 737 | type: string 738 | listen_port: 739 | description: WireGuard device listen port. 740 | example: 51820 741 | format: int32 742 | type: integer 743 | public_key: 744 | description: WireGuard device public key encoded by base64. 745 | example: QFjZjxa2sgwnmGT4NqyRoeNk31AlHjVxHNEH/qY/2no= 746 | type: string 747 | firewall_mark: 748 | description: WireGuard device firewall mark. 749 | example: 10 750 | format: int32 751 | type: integer 752 | networks: 753 | description: IPv4 or IPv6 addresses in CIDR notation 754 | example: 755 | - 10.71.25.1/24 756 | - fd42:21:21::1/64 757 | items: 758 | type: string 759 | type: array 760 | peers_count: 761 | description: WireGuard device's peers count 762 | example: 10 763 | format: int32 764 | type: integer 765 | total_receive_bytes: 766 | description: WireGuard device's peers total receive bytes 767 | example: 59984733 768 | format: int64 769 | type: integer 770 | total_transmit_bytes: 771 | description: WireGuard device's peers total transmit bytes 772 | example: 45331987 773 | format: int64 774 | type: integer 775 | required: 776 | - firewall_mark 777 | - listen_port 778 | - name 779 | - networks 780 | - peers_count 781 | - private_key 782 | - public_key 783 | - total_receive_bytes 784 | - total_transmit_bytes 785 | type: object 786 | PeerCreateOrUpdateRequest: 787 | description: Peer params that might be used due to creation or updation process 788 | example: 789 | public_key: qnPJFozrAnrTjap5VjO30bUeLvhkZEEZx48w2RqMpRA= 790 | endpoint: 1.2.3.4:2345 791 | allowed_ips: 792 | - 10.71.25.51/32 793 | - fd42:21:21::51/128 794 | persistent_keepalive_interval: 25s 795 | private_key: gD89VQVXPAEpXuIyHOujw1wF4njIBtaSLvqAatBAuWY= 796 | preshared_key: c2m+JtxpcRP6pztdDFRHnOx75SI+QyBDba1+BEbQaiA= 797 | properties: 798 | private_key: 799 | description: Base64 encoded private key. If present it will be stored in 800 | persistent storage. 801 | example: gD89VQVXPAEpXuIyHOujw1wF4njIBtaSLvqAatBAuWY= 802 | nullable: true 803 | type: string 804 | public_key: 805 | description: Base64 encoded public key 806 | example: qnPJFozrAnrTjap5VjO30bUeLvhkZEEZx48w2RqMpRA= 807 | nullable: true 808 | type: string 809 | preshared_key: 810 | description: Base64 encoded preshared key 811 | example: c2m+JtxpcRP6pztdDFRHnOx75SI+QyBDba1+BEbQaiA= 812 | nullable: true 813 | type: string 814 | allowed_ips: 815 | description: Peer's allowed ips, it might be any of IPv4 or IPv6 addresses 816 | in CIDR notation 817 | example: 818 | - 10.71.25.51/32 819 | - fd42:21:21::51/128 820 | items: 821 | type: string 822 | nullable: true 823 | type: array 824 | persistent_keepalive_interval: 825 | description: Peer's persistend keepalive interval. Valid time units are 826 | "ns", "us" (or "µs"), "ms", "s", "m", "h". 827 | example: 25s 828 | type: string 829 | endpoint: 830 | description: Peer's endpoint in host:port format 831 | example: 1.2.3.4:2345 832 | type: string 833 | type: object 834 | Peer: 835 | description: Information about wireguard peer. 836 | example: 837 | public_key: 0DGpyohLU+T1qAemWVWsNd1nwy3ZBAG7U4JJ/ZA+fWA= 838 | endpoint: 1.2.3.4:2345 839 | allowed_ips: 840 | - 10.71.25.51/32 841 | - fd42:21:21::51/128 842 | last_handshake_time: 2000-01-23T04:56:07.000+00:00 843 | url_safe_public_key: 0DGpyohLU-T1qAemWVWsNd1nwy3ZBAG7U4JJ_ZA-fWA 844 | persistent_keepalive_interval: 25s 845 | receive_bytes: 587732 846 | transmit_bytes: 432445 847 | preshared_key: c2m+JtxpcRP6pztdDFRHnOx75SI+QyBDba1+BEbQaiA= 848 | properties: 849 | public_key: 850 | description: Base64 encoded public key 851 | example: 0DGpyohLU+T1qAemWVWsNd1nwy3ZBAG7U4JJ/ZA+fWA= 852 | type: string 853 | url_safe_public_key: 854 | description: URL safe base64 encoded public key. It is usefull to use in 855 | peers api endpoint. 856 | example: 0DGpyohLU-T1qAemWVWsNd1nwy3ZBAG7U4JJ_ZA-fWA 857 | type: string 858 | preshared_key: 859 | description: Base64 encoded preshared key 860 | example: c2m+JtxpcRP6pztdDFRHnOx75SI+QyBDba1+BEbQaiA= 861 | type: string 862 | allowed_ips: 863 | description: Peer's allowed ips, it might be any of IPv4 or IPv6 addresses 864 | in CIDR notation 865 | example: 866 | - 10.71.25.51/32 867 | - fd42:21:21::51/128 868 | items: 869 | type: string 870 | type: array 871 | last_handshake_time: 872 | description: Peer's last handshake time formated in RFC3339 873 | format: date-time 874 | type: string 875 | persistent_keepalive_interval: 876 | description: Peer's persistend keepalive interval in 877 | example: 25s 878 | type: string 879 | endpoint: 880 | description: Peer's endpoint in host:port format 881 | example: 1.2.3.4:2345 882 | type: string 883 | receive_bytes: 884 | description: Peer's receive bytes 885 | example: 587732 886 | format: int64 887 | type: integer 888 | transmit_bytes: 889 | description: Peer's transmit bytes 890 | example: 432445 891 | format: int64 892 | type: integer 893 | required: 894 | - allowed_ips 895 | - endpoint 896 | - last_handshake_time 897 | - persistent_keepalive_interval 898 | - public_key 899 | - receive_bytes 900 | - transmit_bytes 901 | - url_safe_public_key 902 | type: object 903 | securitySchemes: 904 | bearerAuth: 905 | description: Authorization token 906 | scheme: bearer 907 | type: http 908 | --------------------------------------------------------------------------------