diff --git a/.gitignore b/.gitignore index dbd10f7..a9326e1 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,7 @@ website/coverage website/build # misc +.env website/.DS_Store website/.env.local website/.env.development.local diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..46d85f0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,41 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.1.0] + +### Added + +- Added support for an admin account. An admin can see all devices registered + with the server. +- Added support for networking isolation modes. You can now allow/deny VPN LAN, + Server LAN and internet traffic. Selective network CIDRs can be white listed. +- New docker compose example ([@antoniebou13](https://github.com/Place1/wg-access-server/pull/13)) +- Added a helm chart +- Added a basic kubernetes quickstart.yaml manifest (based on helm template) +- Added a documentation site based on [mkdocs](https://www.mkdocs.org/). Hosted + on github pages (still a wip!) + +## [0.0.9] + +### Changed + +- Some UI/UX improvements + +## [0.0.8] + +### Added + +- Added an embedded DNS proxy + +### Changed + +- Completely re-implemented the auth subsystem to avoid trying to integrate + with Dex. OIDC, Gitlab and Basic auth are supported. + +## [0.0.0] -> [0.0.7] + +MVP :) diff --git a/Dockerfile b/Dockerfile index c531504..4ce92fa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,7 +31,8 @@ RUN go mod download COPY ./proto/ ./proto/ COPY ./main.go ./main.go -COPY ./internal/ ./internal +COPY ./internal/ ./internal/ +COPY ./pkg/ ./pkg/ RUN go build -o server @@ -51,7 +52,5 @@ ENV STORAGE_DIRECTORY="/data" COPY --from=server /code/server /server COPY --from=website /code/build /website/build -HEALTHCHECK --interval=5m --timeout=3s CMD curl -f http://localhost:8000/ || exit 1 - # Command to start the server CMD /server diff --git a/README.md b/README.md index a9fc5b6..71f947c 100644 --- a/README.md +++ b/README.md @@ -2,42 +2,21 @@ ## What is this -This project aims to create a simple VPN solution for developers, -homelab enthusiasts and anyone else feeling adventurous. - -This project offers a single docker container that provides a WireGuard -VPN server and device management web ui. - -You can use wg-access-server's web ui to connect your Linux/Mac/Windows/iOS/Android -devices. The server automatically configure iptables rules to ensure that client VPN traffic -can access the internet via the server's default gateway or configured gateway NIC. -Currently, all VPN clients can route traffic to each other. VPN client isolation via -iptables can be added if there's demand for it. - -wg-access-server embeds a user-space wireguard implementation to simplify -deployment - you just run the container, no kernel setup required. +wg-access-server is a single binary that provides a WireGuard +VPN server and device management web ui. We support user authentication, +_1 click_ device registration that works with Mac, Linux, Windows, Ios and Android +including QR codes. You can configure different network isolation modes for +better control and more. -Support for the kernal's wireguard implementation could be added if -there's demand for it. - -Currently wg-access-server requires `NET_ADMIN` and access to `/dev/net/tun` to create -a user-space virtual network interface ([wikipedia](https://en.wikipedia.org/wiki/TUN/TAP)). +This project aims to deliver a simple VPN solution for developers, +homelab enthusiasts and anyone else feeling adventurous. -wg-access-server also configures iptables and network routes within it's own network -namespace to route client VPN traffic. The container doesn't require host networking -but it can be enabled if you want client VPN traffic to be able to access the host's -network as well. +wg-access-server is a functional but young project. Contributes are welcome! ## Running with Docker Here's a quick command to run the server to try it out. -If you open your browser using your LAN ip address you can even connect your -phone to try it out: for example, i'll open my browser at http://192.168.0.XX:8000 -using the local LAN IP address. - -You can connect to the web server on the local machine browser at http://localhost:8000 - ```bash docker run \ -it \ @@ -45,171 +24,44 @@ docker run \ --cap-add NET_ADMIN \ --device /dev/net/tun:/dev/net/tun \ -v wg-access-server-data:/data \ + -e "WIREGUARD_PRIVATE_KEY=$(wg genkey)" \ -p 8000:8000/tcp \ -p 51820:51820/udp \ place1/wg-access-server ``` -## Running with Docker-Compose - -You modify the docker-compose.yml file for you need then run this following command. - -```bash -docker-compose up -d -``` +If you open your browser using your LAN ip address you can even connect your +phone to try it out: for example, i'll open my browser at http://192.168.0.XX:8000 +using the local LAN IP address. You can connect to the web server on the local machine browser at http://localhost:8000 -## Configuration +## Running with Docker-Compose -You can configure the server using a yaml configuration file. Just mount the file into the container like this: +You modify the docker-compose.yml file for you need then run this following command. ```bash -docker run \ - ... \ - -v $(pwd)/config.yaml:/config.yaml \ - place1/wg-access-server +docker-compose up ``` -If you want to put the config file in a different location in the container you -can set the config file path using: `-e CONFIG=/path/to/config.yaml` - -Here's and example showing the recommended config: - -```yaml -wireguard: - // The WireGuard PrivateKey - // You can generate this value using "$ wg genkey" - // If this value is empty then the server will use an in-memory - // generated key - privateKey: "" -// Auth configures optional authentication backends -// to controll access to the web ui. -// Devices will be managed on a per-user basis if any -// auth backends are configured. -// If no authentication backends are configured then -// the server will not require any authentication. -// It's recommended to make use of basic authentication -// or use an upstream HTTP proxy that enforces authentication -// Optional -auth: - // HTTP Basic Authentication - basic: - // Users is a list of htpasswd encoded username:password pairs - // supports BCrypt, Sha, Ssha, Md5 - // You can create a user using "htpasswd -nB " - users: [] -``` +You can connect to the web server on the local machine browser at http://localhost:8000 -Here's an example showing the all config values: - -```yaml -loglevel: debug -storage: - // Directory that VPN devices (WireGuard peers) - // should be saved under. - // If this value is empty then an InMemory storage - // backend will be used (not recommended). - // Defaults to "/data" inside the docker container - directory: /data -wireguard: - // The network interface name for wireguard - // Optional - interfaceName: wg0 - // The WireGuard PrivateKey - // You can generate this value using "$ wg genkey" - // If this value is empty then the server will use an in-memory - // generated key - privateKey: "" - // ExternalAddress is the address that clients - // use to connect to the wireguard interface - // By default, this will be empty and the web ui - // will use the current page's origin i.e. window.location.origin - // Optional - externalHost: "" - // The WireGuard ListenPort - // Optional - port: 51820 -} `yaml:"wireguard"` -vpn: - // CIDR configures a network address space - // that client (WireGuard peers) will be allocated - // an IP address from. - // Optional - cidr: "10.44.0.0/24" - // GatewayInterface will be used in iptable forwarding - // rules that send VPN traffic from clients to this interface - // Most use-cases will want this interface to have access - // to the outside internet - // If not configured then the server will select the default - // network interface e.g. eth0 - // Optional - gatewayInterface: "" -dns: - // upstream DNS servers. - // that the server-side DNS proxy will forward requests to. - // By default /etc/resolv.conf will be used to find upstream - // DNS servers. - // Optional - upstream: - - "1.1.1.1" -// Auth configures optional authentication backends -// to controll access to the web ui. -// Devices will be managed on a per-user basis if any -// auth backends are configured. -// If no authentication backends are configured then -// the server will not require any authentication. -// It's recommended to make use of basic authentication -// or use an upstream HTTP proxy that enforces authentication -// Optional -auth: - // HTTP Basic Authentication - basic: - // Users is a list of htpasswd encoded username:password pairs - // supports BCrypt, Sha, Ssha, Md5 - // You can create a user using "htpasswd -nB " - users: [] - oidc: - name: "" - issuer: "" - clientID: "" - clientSecret: "" - scopes: "" - redirectURL: "" - // Optionally restrict login to users with an allowed email domain - // if empty or omitted, any email domain will be allowed. - emailDomains: - - example.com - gitlab: - name: "" - baseURL: "" - clientID: "" - clientSecret: "" - redirectURL: "" - // Optionally restrict login to users with an allowed email domain - // if empty or omitted, any email domain will be allowed. - emailDomains: - - example.com -``` +If you open your browser to your machine's LAN IP address you'll be able +to connect your phone using the UI and QR code! ## Screenshots -![Connect iOS](./screenshots/connect-ios.png) +![Devices](https://github.com/Place1/wg-access-server/raw/master/screenshots/devices.png) -![Connect MacOS](./screenshots/connect-macos.png) +![Connect iOS](https://github.com/Place1/wg-access-server/raw/master/screenshots/connect-ios.png) -![Devices](./screenshots/devices.png) +![Connect MacOS](https://github.com/Place1/wg-access-server/raw/master/screenshots/connect-macos.png) -![Sign In](./screenshots/signin.png) +![Sign In](https://github.com/Place1/wg-access-server/raw/master/screenshots/signin.png) -## Roadmap +## Changelog -- [ ] Implement administration features - - administration of all devices - - see when a device last connected - - see owns the device -- [ ] VPN network client isolation -- [ ] ??? PRs, feedback, suggestions welcome +See the [CHANGELOG.md](https://github.com/Place1/wg-access-server/blob/master/CHANGELOG.md) file ## Development diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..0934824 --- /dev/null +++ b/TODO.md @@ -0,0 +1,27 @@ +## Docs +- [x] mkdocs +- [ ] about +- [x] deploying + - [x] simple docker 1 liner + - [x] docker-compose + - [x] kubernetes quickstart + - [x] helm +- [x] configuring + - [x] general + - [x] config file/flag/env +- [ ] how-to-guides + - [ ] docker + docker-compose + - [ ] kubernetes + nginx ingress + - [ ] raspberry-pi + pihole dns + +## Features +- [ ] ARM docker image for raspberry-pi +- [ ] admin + - [x] list all devices + - [ ] remove device +- [x] networking + - [x] isolate clients + - [x] forward to internet only (isolate LAN/WAN) + - [x] allowed networks (configure forwarding to specific CIDRs) + - [x] also limit which CIDRs clients forward + - [x] i.e. only forward to specific server-side LAN and not all internet traffic diff --git a/deploy/helm/wg-access-server/README.md b/deploy/helm/wg-access-server/README.md new file mode 100644 index 0000000..bd11d46 --- /dev/null +++ b/deploy/helm/wg-access-server/README.md @@ -0,0 +1,70 @@ +## Installing the Chart + +To install the chart with the release name `my-release`: + +```bash +$ helm install my-release --repo https://place1.github.io/wg-access-server wg-access-server +``` + +The command deploys wg-access-server on the Kubernetes cluster in the default configuration. The configuration section lists the parameters that can be configured during installation. + +By default an in-memory wireguard private key will be generated and devices will not persist +between pod restarts. + +## Uninstalling the Chart + +To uninstall/delete the my-release deployment: + +```bash +$ helm delete my-release +``` + +The command removes all the Kubernetes components associated with the chart and deletes the release. + +## Example values.yaml + +```yaml +config: + wireguard: + externalHost: ":51820" +wireguard: + config: + privateKey: "" + service: + type: "LoadBalancer" +persistence: + enabled: true +ingress: + enabled: true + hosts: ["vpn.example.com"] + tls: + - hosts: ["vpn.example.com"] + secretName: "tls-wg-access-server" +``` + +## All Configuration + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| config | object | `{}` | inline wg-access-server config (config.yaml) | +| wireguard.config.privateKey | string | "" | A wireguard private key. You can generate one using `$ wg genkey` | +| wireguard.service.type | string | `"ClusterIP"` | | +| ingress.enabled | bool | `false` | | +| ingress.hosts | string | `nil` | | +| ingress.tls | list | `[]` | | +| ingress.annotations | object | `{}` | | +| persistence.enabled | bool | `false` | | +| persistence.size | string | `"100Mi"` | | +| persistence.subPath | string | `""` | | +| persistence.annotations | object | `{}` | | +| persistence.accessModes[0] | string | `"ReadWriteOnce"` | | +| strategy.type | string | `"Recreate"` | | +| resources | object | `{}` | pod cpu/memory resource requests and limits | +| nameOverride | string | `""` | | +| fullnameOverride | string | `""` | | +| affinity | object | `{}` | | +| nodeSelector | object | `{}` | | +| tolerations | list | `[]` | | +| image.pullPolicy | string | `"IfNotPresent"` | | +| image.repository | string | `"place1/wg-access-server"` | | +| imagePullSecrets | list | `[]` | | diff --git a/deploy/helm/wg-access-server/templates/deployment.yaml b/deploy/helm/wg-access-server/templates/deployment.yaml index c40bea7..a8a546a 100644 --- a/deploy/helm/wg-access-server/templates/deployment.yaml +++ b/deploy/helm/wg-access-server/templates/deployment.yaml @@ -35,6 +35,28 @@ spec: - name: wireguard containerPort: 51820 protocol: UDP + env: + {{- if .Values.wireguard.config.privateKey }} + - name: WIREGUARD_PRIVATE_KEY + valueFrom: + secretKeyRef: + name: "{{ $fullName }}" + key: privateKey + {{- end }} + {{- if .Values.web.config.adminUsername }} + - name: ADMIN_USERNAME + valueFrom: + secretKeyRef: + name: "{{ $fullName }}" + key: adminUsername + {{- end}} + {{- if .Values.web.config.adminPassword }} + - name: ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: "{{ $fullName }}" + key: adminPassword + {{- end}} volumeMounts: - name: tun mountPath: /dev/net/tun diff --git a/deploy/helm/wg-access-server/templates/ingress.yaml b/deploy/helm/wg-access-server/templates/ingress.yaml index 19c6e91..9b63f61 100644 --- a/deploy/helm/wg-access-server/templates/ingress.yaml +++ b/deploy/helm/wg-access-server/templates/ingress.yaml @@ -26,11 +26,9 @@ spec: - host: {{ .host | quote }} http: paths: - {{- range .paths }} - - path: {{ . }} + - path: / backend: - serviceName: {{ $fullName }} + serviceName: {{ $fullName }}-web servicePort: 80 - {{- end }} {{- end }} {{- end }} diff --git a/deploy/helm/wg-access-server/templates/secret.yaml b/deploy/helm/wg-access-server/templates/secret.yaml new file mode 100644 index 0000000..baf61c2 --- /dev/null +++ b/deploy/helm/wg-access-server/templates/secret.yaml @@ -0,0 +1,18 @@ +{{- $fullName := include "wg-access-server.fullname" . -}} +{{- if .Values.wireguard.config.privateKey }} +apiVersion: v1 +kind: Secret +metadata: + name: "{{ $fullName }}" + labels: + {{- include "wg-access-server.labels" . | nindent 4 }} +type: Opaque +data: + privateKey: {{ .Values.wireguard.config.privateKey | b64enc | quote }} + {{- if .Values.web.config.adminUsername }} + adminUsername: {{ .Values.web.config.adminUsername | b64enc | quote }} + {{- end }} + {{- if .Values.web.config.adminPassword }} + adminPassword: {{ .Values.web.config.adminPassword | b64enc | quote }} + {{- end }} +{{- end }} diff --git a/deploy/helm/wg-access-server/templates/service.yaml b/deploy/helm/wg-access-server/templates/service.yaml index 6869197..52bdaca 100644 --- a/deploy/helm/wg-access-server/templates/service.yaml +++ b/deploy/helm/wg-access-server/templates/service.yaml @@ -6,7 +6,7 @@ metadata: labels: {{- include "wg-access-server.labels" . | nindent 4 }} spec: - type: {{ .Values.web.service.type }} + type: ClusterIP ports: - port: 80 targetPort: http diff --git a/deploy/helm/wg-access-server/values.yaml b/deploy/helm/wg-access-server/values.yaml index 5d47ecf..ffabba1 100644 --- a/deploy/helm/wg-access-server/values.yaml +++ b/deploy/helm/wg-access-server/values.yaml @@ -1,17 +1,14 @@ # wg-access-server config -config: - wireguard: - # The WireGuard PrivateKey - # You can generate this value using "$ wg genkey" - # If this value is empty then the server will use an in-memory - # generated key - privateKey: "" +config: {} web: - service: - type: ClusterIP + config: + adminUsername: "" + adminPassword: "" wireguard: + config: + privateKey: "" service: type: ClusterIP diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..95f1434 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,141 @@ +# Configuration + +## Environment Variables + +| Variable | Description | +|-----------------------|-------------| +| CONFIG | Set the config file path | +| WIREGUARD_PRIVATE_KEY | Set the wireguard private key | +| STORAGE_DIRECTORY | Set the directory where device config will be persisted | +| ADMIN_USERNAME | Set the username (subject) for the admin account | +| ADMIN_PASSWORD | Set the admin account's password. The admin account will be a basic-auth user. Leave blank if your admin username authenticates via a configured authentication backend. | +| UPSTREAM_DNS | Set the upstream DNS server to proxy client DNS requests to. If empty, resolv.conf will be respected. | +| LOG_LEVEL | Set the server's log level (debug, **info**, error, critical) | +| DISABLE_METADATA | If true, the server will not record device level metadata such as the last handshake time, tx/rx data size | + +## CLI Flags + +All environment variables can be configured via a +CLI flag as well. + +For example you can configure `STORAGE_DIRECTORY` by passing `--storage-directory=""`. + +## Config File (config.yaml) + +Here's an annotated config file example: + +```yaml +loglevel: debug +storage: + # Directory that VPN devices (WireGuard peers) + # should be saved under. + # If this value is empty then an InMemory storage + # backend will be used (not recommended). + # Defaults to "/data" inside the docker container + directory: /data +wireguard: + # The network interface name for wireguard + # Optional + interfaceName: wg0 + # The WireGuard PrivateKey + # You can generate this value using "$ wg genkey" + # If this value is empty then the server will use an in-memory + # generated key + privateKey: "" + # ExternalAddress is the address that clients + # use to connect to the wireguard interface + # By default, this will be empty and the web ui + # will use the current page's origin i.e. window.location.origin + # Optional + externalHost: "" + # The WireGuard ListenPort + # Optional + port: 51820 +} `yaml:"wireguard"` +vpn: + # CIDR configures a network address space + # that client (WireGuard peers) will be allocated + # an IP address from. + # Optional + cidr: "10.44.0.0/24" + # GatewayInterface will be used in iptable forwarding + # rules that send VPN traffic from clients to this interface + # Most use-cases will want this interface to have access + # to the outside internet + # If not configured then the server will select the default + # network interface e.g. eth0 + # Optional + gatewayInterface: "" + // Rules allows you to configure what level + // of network isolation should be enfoced. + rules: + # AllowVPNLAN enables routing between VPN clients + # i.e. allows the VPN to work like a LAN. + # true by default + # Optional + allowVPNLAN: true + # AllowServerLAN enables routing to private IPv4 + # address ranges. Enabling this will allow VPN clients + # to access networks on the server's LAN. + # true by default + # Optional + allowServerLAN: true + # AllowInternet enables routing of all traffic + # to the public internet. + # true by default + # Optional + allowInternet: true + # AllowedNetworks allows you to whitelist a partcular + # network CIDR. This is useful if you want to block + # access to the Server's LAN but allow access to a few + # specific IPs or a small range. + # e.g. "192.0.2.0/24" or "192.0.2.10/32". + # no networks are whitelisted by default (empty array) + # Optional + allowedNetworks: [] +dns: + # upstream DNS servers. + # that the server-side DNS proxy will forward requests to. + # By default /etc/resolv.conf will be used to find upstream + # DNS servers. + # Optional + upstream: + - "1.1.1.1" +# Auth configures optional authentication backends +# to controll access to the web ui. +# Devices will be managed on a per-user basis if any +# auth backends are configured. +# If no authentication backends are configured then +# the server will not require any authentication. +# It's recommended to make use of basic authentication +# or use an upstream HTTP proxy that enforces authentication +# Optional +auth: + # HTTP Basic Authentication + basic: + # Users is a list of htpasswd encoded username:password pairs + # supports BCrypt, Sha, Ssha, Md5 + # You can create a user using "htpasswd -nB " + users: [] + oidc: + name: "" + issuer: "" + clientID: "" + clientSecret: "" + scopes: "" + redirectURL: "" + # Optionally restrict login to users with an allowed email domain + # if empty or omitted, any email domain will be allowed. + emailDomains: + - example.com + gitlab: + name: "" + baseURL: "" + clientID: "" + clientSecret: "" + redirectURL: "" + # Optionally restrict login to users with an allowed email domain + # if empty or omitted, any email domain will be allowed. + emailDomains: + - example.com +``` diff --git a/docs/deployment/1-docker.md b/docs/deployment/1-docker.md new file mode 100644 index 0000000..af0099c --- /dev/null +++ b/docs/deployment/1-docker.md @@ -0,0 +1,14 @@ +# Docker + +## TL;DR; + +Here's a one-liner to run wg-access-server: + +```bash +docker run --rm -it + --cap-add NET_ADMIN \ + --device /dev/net/tun:/dev/net/tun \ + -p 8000:8000/tcp \ + -p 51820:51820/udp \ + place1/wg-access-server +``` diff --git a/docs/deployment/2-docker-compose.md b/docs/deployment/2-docker-compose.md new file mode 100644 index 0000000..f227c90 --- /dev/null +++ b/docs/deployment/2-docker-compose.md @@ -0,0 +1,11 @@ +# Docker Compose + +You can run wg-access-server using the following example +docker Docker Compose file. + +Checkout the [configuration docs](../configuration.md) to learn how wg-access-server +can be configured. + +```yaml +{!../docker-compose.yml!} +``` diff --git a/docs/deployment/3-kubernetes.md b/docs/deployment/3-kubernetes.md new file mode 100644 index 0000000..3c46f03 --- /dev/null +++ b/docs/deployment/3-kubernetes.md @@ -0,0 +1,3 @@ +# Helm Chart + +{!../deploy/helm/wg-access-server/README.md!} diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..842936c --- /dev/null +++ b/docs/index.md @@ -0,0 +1,3 @@ +# Welcome + +{!../README.md!} diff --git a/go.mod b/go.mod index 622d741..ea178a1 100644 --- a/go.mod +++ b/go.mod @@ -23,14 +23,14 @@ require ( github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pkg/errors v0.8.1 - github.com/place1/wg-embed v0.0.0-20200220103052-288c50323e73 + github.com/place1/wg-embed v0.1.0 github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect github.com/rs/cors v1.7.0 // indirect github.com/sirupsen/logrus v1.4.2 github.com/smartystreets/goconvey v1.6.4 // indirect github.com/tg123/go-htpasswd v1.0.0 github.com/vishvananda/netlink v1.0.0 - golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 // indirect + golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 golang.org/x/net v0.0.0-20200202094626-16171245cfb2 // indirect golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 // indirect @@ -42,4 +42,7 @@ require ( gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/square/go-jose.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.2.2 + gotest.tools v2.2.0+incompatible // indirect ) + +// replace github.com/place1/wg-embed => ../wg-embed diff --git a/go.sum b/go.sum index f76b4d4..8554b13 100644 --- a/go.sum +++ b/go.sum @@ -3,7 +3,6 @@ cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/Sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 h1:Hs82Z41s6SdL1CELW+XaDYmOH4hkBN4/N9og/AsOv7E= @@ -91,8 +90,8 @@ github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaR github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/place1/wg-embed v0.0.0-20200220103052-288c50323e73 h1:vqidjPSGwdYc3zR3O1+0HrME8By/CGbRXsLY45xFxyI= -github.com/place1/wg-embed v0.0.0-20200220103052-288c50323e73/go.mod h1:Nse1dFm7Lq10qbz5FlM3vjZch0Dif6xjJL1OfrXuozc= +github.com/place1/wg-embed v0.1.0 h1:24tOYkcE++zI4lvXUa464KUwbyi46EZ7zmQm8UyLvAI= +github.com/place1/wg-embed v0.1.0/go.mod h1:Nse1dFm7Lq10qbz5FlM3vjZch0Dif6xjJL1OfrXuozc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU= @@ -220,5 +219,7 @@ gopkg.in/square/go-jose.v2 v2.4.0 h1:0kXPskUMGAXXWJlP05ktEMOV0vmzFQUWw6d+aZJQU8A gopkg.in/square/go-jose.v2 v2.4.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/config/config.go b/internal/config/config.go index fb04c44..97e6f9b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -10,18 +10,23 @@ import ( "gopkg.in/yaml.v2" - "github.com/place1/wg-access-server/internal/auth/authconfig" + "github.com/place1/wg-access-server/internal/network" + "github.com/place1/wg-access-server/pkg/authnz/authconfig" "github.com/vishvananda/netlink" "github.com/pkg/errors" "github.com/sirupsen/logrus" + "golang.org/x/crypto/bcrypt" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "gopkg.in/alecthomas/kingpin.v2" ) type AppConfig struct { - LogLevel string `yaml:"loglevel"` - Storage struct { + LogLevel string `yaml:"loglevel"` + DisableMetadata bool `yaml:"disableMetadata"` + AdminSubject string `yaml:"adminSubject"` + AdminPassword string `yaml:"adminPassword"` + Storage struct { // Directory that VPN devices (WireGuard peers) // should be saved under. // If this value is empty then an InMemory storage @@ -60,6 +65,9 @@ type AppConfig struct { // Most use-cases will want this interface to have access // to the outside internet GatewayInterface string `yaml:"gatewayInterface"` + // Rules allows you to configure what level + // of network isolation should be enfoced. + Rules *network.NetworkRules `yaml:"rules"` } DNS struct { Upstream []string `yaml:"upstream"` @@ -70,41 +78,58 @@ type AppConfig struct { // auth backends are configured. // If no authentication backends are configured then // the server will not require any authentication. - Auth *authconfig.AuthConfig `yaml:"auth"` + Auth authconfig.AuthConfig `yaml:"auth"` } var ( - app = kingpin.New("wg-access-server", "An all-in-one WireGuard Access Server & VPN solution") - configPath = app.Flag("config", "Path to a config file").OverrideDefaultFromEnvar("CONFIG").String() + app = kingpin.New("wg-access-server", "An all-in-one WireGuard Access Server & VPN solution") + configPath = app.Flag("config", "Path to a config file").Envar("CONFIG").String() + logLevel = app.Flag("log-level", "Log level (debug, info, error)").Envar("LOG_LEVEL").Default("info").String() + storagePath = app.Flag("storage-directory", "Path to a storage directory").Envar("STORAGE_DIRECTORY").String() + privateKey = app.Flag("wireguard-private-key", "Wireguard private key").Envar("WIREGUARD_PRIVATE_KEY").String() + disableMetadata = app.Flag("disable-metadata", "Disable metadata collection (i.e. metrics)").Envar("DISABLE_METADATA").Default("false").Bool() + adminUsername = app.Flag("admin-username", "Admin username (defaults to admin)").Envar("ADMIN_USERNAME").String() + adminPassword = app.Flag("admin-password", "Admin password (provide plaintext, stored in-memory only)").Envar("ADMIN_PASSWORD").String() + upstreamDNS = app.Flag("upstream-dns", "An upstream DNS server to proxy DNS traffic to").Envar("UPSTREAM_DNS").String() ) func Read() *AppConfig { kingpin.MustParse(app.Parse(os.Args[1:])) config := AppConfig{} - config.LogLevel = "info" + config.LogLevel = *logLevel config.WireGuard.InterfaceName = "wg0" config.WireGuard.Port = 51820 config.VPN.CIDR = "10.44.0.0/24" - - if *configPath != "" { - if b, err := ioutil.ReadFile(*configPath); err == nil { - if err := yaml.Unmarshal(b, &config); err != nil { - logrus.Fatal(errors.Wrap(err, "failed to bind configuration file")) - } + config.DisableMetadata = *disableMetadata + config.Storage.Directory = *storagePath + config.WireGuard.PrivateKey = *privateKey + if adminPassword != nil { + config.AdminPassword = *adminPassword + config.AdminSubject = *adminUsername + if config.AdminSubject == "" { + config.AdminSubject = "admin" } } - if v, ok := os.LookupEnv("LOG_LEVEL"); ok { - config.LogLevel = v + if upstreamDNS != nil { + config.DNS.Upstream = []string{*upstreamDNS} } - if v, ok := os.LookupEnv("STORAGE_DIRECTORY"); ok { - config.Storage.Directory = v + if config.VPN.Rules == nil { + config.VPN.Rules = &network.NetworkRules{ + AllowVPNLAN: true, + AllowServerLAN: true, + AllowInternet: true, + } } - if v, ok := os.LookupEnv("WIREGUARD_PRIVATE_KEY"); ok { - config.WireGuard.PrivateKey = v + if *configPath != "" { + if b, err := ioutil.ReadFile(*configPath); err == nil { + if err := yaml.Unmarshal(b, &config); err != nil { + logrus.Fatal(errors.Wrap(err, "failed to bind configuration file")) + } + } } level, err := logrus.ParseLevel(config.LogLevel) @@ -120,6 +145,10 @@ func Read() *AppConfig { }, }) + if config.DisableMetadata { + logrus.Info("Metadata collection has been disabled. No metrics or device connectivity information will be recorded or shown") + } + if config.VPN.GatewayInterface == "" { iface, err := defaultInterface() if err != nil { @@ -145,13 +174,22 @@ func Read() *AppConfig { if err != nil { logrus.Fatal(errors.Wrap(err, "failed to get absolute path to storage directory")) } + os.MkdirAll(config.Storage.Directory, 0700) } - return &config -} + if config.AdminPassword != "" { + if config.Auth.Basic == nil { + config.Auth.Basic = &authconfig.BasicAuthConfig{} + } + // htpasswd.AcceptBcrypt(config.AdminPassword) + pw, err := bcrypt.GenerateFromPassword([]byte(config.AdminPassword), bcrypt.DefaultCost) + if err != nil { + logrus.Fatal(errors.Wrap(err, "failed to generate a bcrypt hash for the provided admin password")) + } + config.Auth.Basic.Users = append(config.Auth.Basic.Users, fmt.Sprintf("%s:%s", config.AdminSubject, string(pw))) + } -func (config *AppConfig) IsAuthEnabled() bool { - return config.Auth != nil + return &config } func defaultInterface() (string, error) { diff --git a/internal/devices/devices.go b/internal/devices/devices.go index e7de1ca..937643e 100644 --- a/internal/devices/devices.go +++ b/internal/devices/devices.go @@ -25,7 +25,8 @@ func New(iface string, s storage.Storage, cidr string) *DeviceManager { return &DeviceManager{iface, s, cidr} } -func (d *DeviceManager) Sync() error { +func (d *DeviceManager) StartSync(disableMetadataCollection bool) error { + // sync devices from storage once devices, err := d.ListDevices("") if err != nil { return errors.Wrap(err, "failed to list devices") @@ -35,6 +36,12 @@ func (d *DeviceManager) Sync() error { logrus.Warn(errors.Wrapf(err, "failed to sync device '%s' (ignoring)", device.Name)) } } + + // start the metrics loop + if !disableMetadataCollection { + go metadataLoop(d) + } + return nil } @@ -56,7 +63,7 @@ func (d *DeviceManager) AddDevice(user string, name string, publicKey string) (* CreatedAt: time.Now(), } - if err := d.storage.Save(key(user, device.Name), device); err != nil { + if err := d.SaveDevice(device); err != nil { return nil, errors.Wrap(err, "failed to save the new device") } @@ -67,6 +74,14 @@ func (d *DeviceManager) AddDevice(user string, name string, publicKey string) (* return device, nil } +func (d *DeviceManager) SaveDevice(device *storage.Device) error { + return d.storage.Save(key(device.Owner, device.Name), device) +} + +func (d *DeviceManager) ListAllDevices() ([]*storage.Device, error) { + return d.storage.List("") +} + func (d *DeviceManager) ListDevices(user string) ([]*storage.Device, error) { prefix := "" if user != "" { diff --git a/internal/devices/metadata.go b/internal/devices/metadata.go new file mode 100644 index 0000000..9028360 --- /dev/null +++ b/internal/devices/metadata.go @@ -0,0 +1,46 @@ +package devices + +import ( + "time" + + "github.com/pkg/errors" + "github.com/place1/wg-embed/pkg/wgembed" + "github.com/sirupsen/logrus" +) + +func metadataLoop(d *DeviceManager) { + for { + syncMetrics(d) + time.Sleep(5 * time.Second) + } +} + +func syncMetrics(d *DeviceManager) { + devices, err := d.ListAllDevices() + if err != nil { + logrus.Warn(errors.Wrap(err, "failed to list devices - metrics cannot be recorded")) + return + } + peers, err := wgembed.ListPeers(d.iface) + if err != nil { + logrus.Warn(errors.Wrap(err, "failed to list peers - metrics cannot be recorded")) + return + } + for _, peer := range peers { + for _, device := range devices { + if peer.PublicKey.String() == device.PublicKey { + device.ReceiveBytes = peer.ReceiveBytes + device.TransmitBytes = peer.TransmitBytes + if !peer.LastHandshakeTime.IsZero() { + device.LastHandshakeTime = &peer.LastHandshakeTime + } + if peer.Endpoint != nil { + device.Endpoint = peer.Endpoint.IP.String() + } + if err := d.SaveDevice(device); err != nil { + logrus.Debug(errors.Wrap(err, "failed to update device metadata")) + } + } + } + } +} diff --git a/internal/network/network.go b/internal/network/network.go new file mode 100644 index 0000000..26e9949 --- /dev/null +++ b/internal/network/network.go @@ -0,0 +1,174 @@ +package network + +import ( + "fmt" + "net" + + "github.com/coreos/go-iptables/iptables" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/vishvananda/netlink" +) + +func ServerVPNIP(cidr string) *net.IPNet { + vpnip, vpnsubnet := MustParseCIDR(cidr) + vpnsubnet.IP = nextIP(vpnip.Mask(vpnsubnet.Mask)) + return vpnsubnet +} + +func ConfigureRouting(wgIface string, cidr string) error { + // Networking configuration (ip links and route tables) + // to ensure that network traffic in the VPN subnet + // moves through the wireguard interface + link, err := netlink.LinkByName(wgIface) + if err != nil { + return errors.Wrap(err, "failed to find wireguard interface") + } + vpnip := ServerVPNIP(cidr) + logrus.Infof("server VPN subnet IP is %s", vpnip.String()) + addr, err := netlink.ParseAddr(vpnip.String()) + if err != nil { + return errors.Wrap(err, "failed to parse subnet address") + } + if err := netlink.AddrAdd(link, addr); err != nil { + logrus.Warn(errors.Wrap(err, "failed to add subnet to wireguard interface")) + } + if err := netlink.LinkSetUp(link); err != nil { + logrus.Warn(errors.Wrap(err, "failed to bring wireguard interface up")) + } + return nil +} + +type NetworkRules struct { + // AllowVPNLAN enables routing between VPN clients + // i.e. allows the VPN to work like a LAN. + // true by default + AllowVPNLAN bool `yaml:"allowVPNLAN"` + // AllowServerLAN enables routing to private IPv4 + // address ranges. Enabling this will allow VPN clients + // to access networks on the server's LAN. + // true by default + AllowServerLAN bool `yaml:"allowServerLAN"` + // AllowInternet enables routing of all traffic + // to the public internet. + // true by default + AllowInternet bool `yaml:"allowInternet"` + // AllowedNetworks allows you to whitelist a partcular + // network CIDR. This is useful if you want to block + // access to the Server's LAN but allow access to a few + // specific IPs or a small range. + // e.g. "192.0.2.0/24" or "192.0.2.10/32". + // no networks are whitelisted by default (empty array) + AllowedNetworks []string `yaml:"allowedNetworks"` +} + +func ConfigureForwarding(wgIface string, gatewayIface string, cidr string, rules NetworkRules) error { + // Networking configuration (iptables) configuration + // to ensure that traffic from clients the wireguard interface + // is sent to the provided network interface + ipt, err := iptables.New() + if err != nil { + return errors.Wrap(err, "failed to init iptables") + } + + // Cleanup our chains first so that we don't leak + // iptable rules when the network configuration changes. + ipt.ClearChain("filter", "WG_ACCESS_SERVER_FORWARD") + ipt.ClearChain("nat", "WG_ACCESS_SERVER_POSTROUTING") + + // Create our own chain for forwarding rules + ipt.NewChain("filter", "WG_ACCESS_SERVER_FORWARD") + ipt.AppendUnique("filter", "FORWARD", "-j", "WG_ACCESS_SERVER_FORWARD") + + // Create our own chain for postrouting rules + ipt.NewChain("nat", "WG_ACCESS_SERVER_POSTROUTING") + ipt.AppendUnique("nat", "POSTROUTING", "-j", "WG_ACCESS_SERVER_POSTROUTING") + + if err := ConfigureRouting(wgIface, cidr); err != nil { + logrus.Error(errors.Wrap(err, "failed to configure interface")) + } + + // https://simple.wikipedia.org/wiki/Private_network + privateCIDRs := []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"} + + // White listed networks + if len(rules.AllowedNetworks) != 0 { + for _, subnet := range rules.AllowedNetworks { + if err := ipt.AppendUnique("filter", "WG_ACCESS_SERVER_FORWARD", "-s", cidr, "-d", subnet, "-j", "ACCEPT"); err != nil { + return errors.Wrap(err, "failed to set ip tables rule") + } + } + } + + // VPN LAN + if rules.AllowVPNLAN { + if err := ipt.AppendUnique("filter", "WG_ACCESS_SERVER_FORWARD", "-s", cidr, "-d", cidr, "-j", "ACCEPT"); err != nil { + return errors.Wrap(err, "failed to set ip tables rule") + } + } else { + if err := ipt.AppendUnique("filter", "WG_ACCESS_SERVER_FORWARD", "-s", cidr, "-d", cidr, "-j", "REJECT"); err != nil { + return errors.Wrap(err, "failed to set ip tables rule") + } + } + + // Server LAN + for _, privateCIDR := range privateCIDRs { + if err := ipt.AppendUnique("filter", "WG_ACCESS_SERVER_FORWARD", "-s", cidr, "-d", privateCIDR, "-j", boolToRule(rules.AllowServerLAN)); err != nil { + return errors.Wrap(err, "failed to set ip tables rule") + } + } + + // Internet + if rules.AllowInternet && gatewayIface != "" { + // TODO: do we actually need to specify a gateway interface? + // I suppose i neet to refresh my knowledge of nat. + // if you're reading this please open a Github issue and help teach me nat and iptables :P + if err := ipt.AppendUnique("filter", "WG_ACCESS_SERVER_FORWARD", "-s", cidr, "-i", gatewayIface, "-o", wgIface, "-j", "ACCEPT"); err != nil { + return errors.Wrap(err, "failed to set ip tables rule") + } + if err := ipt.AppendUnique("filter", "WG_ACCESS_SERVER_FORWARD", "-s", cidr, "-i", wgIface, "-o", gatewayIface, "-j", "ACCEPT"); err != nil { + return errors.Wrap(err, "failed to set ip tables rule") + } + if err := ipt.AppendUnique("nat", "WG_ACCESS_SERVER_POSTROUTING", "-s", cidr, "-o", gatewayIface, "-j", "MASQUERADE"); err != nil { + return errors.Wrap(err, "failed to set ip tables rule") + } + } else { + if err := ipt.AppendUnique("filter", "WG_ACCESS_SERVER_FORWARD", "-s", cidr, "-j", "REJECT"); err != nil { + return errors.Wrap(err, "failed to set ip tables rule") + } + } + + return nil +} + +func MustParseCIDR(cidr string) (net.IP, *net.IPNet) { + ip, ipnet, err := net.ParseCIDR(cidr) + if err != nil { + panic(err) + } + return ip, ipnet +} + +func MustParseIP(ip string) net.IP { + netip, _ := MustParseCIDR(fmt.Sprintf("%s/32", ip)) + return netip +} + +func nextIP(ip net.IP) net.IP { + next := make([]byte, len(ip)) + copy(next, ip) + for j := len(next) - 1; j >= 0; j-- { + next[j]++ + if next[j] > 0 { + break + } + } + return next +} + +func boolToRule(accept bool) string { + if accept { + return "ACCEPT" + } + return "REJECT" +} diff --git a/internal/services/converters.go b/internal/services/converters.go index 2bcb3ac..956f556 100644 --- a/internal/services/converters.go +++ b/internal/services/converters.go @@ -13,8 +13,11 @@ func TimestampToTime(value *timestamp.Timestamp) time.Time { return time.Unix(value.Seconds, int64(value.Nanos)) } -func TimeToTimestamp(value time.Time) *timestamp.Timestamp { - t, err := ptypes.TimestampProto(value) +func TimeToTimestamp(value *time.Time) *timestamp.Timestamp { + if value == nil { + return nil + } + t, err := ptypes.TimestampProto(*value) if err != nil { logrus.Error("bad time value") t = ptypes.TimestampNow() diff --git a/internal/services/device_service.go b/internal/services/device_service.go index 2a0f29c..90b4889 100644 --- a/internal/services/device_service.go +++ b/internal/services/device_service.go @@ -2,8 +2,9 @@ package services import ( "context" + "time" - "github.com/place1/wg-access-server/internal/auth/authsession" + "github.com/place1/wg-access-server/pkg/authnz/authsession" "github.com/golang/protobuf/ptypes/empty" "github.com/place1/wg-access-server/internal/devices" @@ -63,13 +64,46 @@ func (d *DeviceService) DeleteDevice(ctx context.Context, req *proto.DeleteDevic return &empty.Empty{}, nil } +func (d *DeviceService) ListAllDevices(ctx context.Context, req *proto.ListAllDevicesReq) (*proto.ListAllDevicesRes, error) { + user, err := authsession.CurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.PermissionDenied, "not authenticated") + } + + if !user.Claims.Contains("admin") { + return nil, status.Errorf(codes.PermissionDenied, "must be an admin") + } + + devices, err := d.DeviceManager.ListAllDevices() + if err != nil { + logrus.Error(err) + return nil, status.Errorf(codes.Internal, "failed to retrieve devices") + } + + return &proto.ListAllDevicesRes{ + Items: mapDevices(devices), + }, nil +} + func mapDevice(d *storage.Device) *proto.Device { return &proto.Device{ - Name: d.Name, - Owner: d.Owner, - PublicKey: d.PublicKey, - Address: d.Address, - CreatedAt: TimeToTimestamp(d.CreatedAt), + Name: d.Name, + Owner: d.Owner, + PublicKey: d.PublicKey, + Address: d.Address, + CreatedAt: TimeToTimestamp(&d.CreatedAt), + LastHandshakeTime: TimeToTimestamp(d.LastHandshakeTime), + ReceiveBytes: d.ReceiveBytes, + TransmitBytes: d.TransmitBytes, + Endpoint: d.Endpoint, + /** + * Wireguard is a connectionless UDP protocol - data is only + * sent over the wire when the client is sending real traffic. + * Wireguard has no keep alive packets by default to remain as + * silent as possible. + * + */ + Connected: isConnected(d.LastHandshakeTime), } } @@ -80,3 +114,10 @@ func mapDevices(devices []*storage.Device) []*proto.Device { } return items } + +func isConnected(lastHandshake *time.Time) bool { + if lastHandshake == nil { + return false + } + return lastHandshake.After(time.Now().Add(-1 * time.Minute)) +} diff --git a/internal/services/network.go b/internal/services/network.go deleted file mode 100644 index 92f2e01..0000000 --- a/internal/services/network.go +++ /dev/null @@ -1,88 +0,0 @@ -package services - -import ( - "fmt" - "net" - - "github.com/coreos/go-iptables/iptables" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - "github.com/vishvananda/netlink" -) - -func ServerVPNIP(cidr string) *net.IPNet { - vpnip, vpnsubnet := MustParseCIDR(cidr) - vpnsubnet.IP = nextIP(vpnip.Mask(vpnsubnet.Mask)) - return vpnsubnet -} - -func ConfigureRouting(wgIface string, cidr string) error { - // Networking configuration (ip links and route tables) - // to ensure that network traffic in the VPN subnet - // moves through the wireguard interface - link, err := netlink.LinkByName(wgIface) - if err != nil { - return errors.Wrap(err, "failed to find wireguard interface") - } - vpnip := ServerVPNIP(cidr) - logrus.Infof("server VPN subnet IP is %s", vpnip.String()) - addr, err := netlink.ParseAddr(vpnip.String()) - if err != nil { - return errors.Wrap(err, "failed to parse subnet address") - } - if err := netlink.AddrAdd(link, addr); err != nil { - logrus.Warn(errors.Wrap(err, "failed to add subnet to wireguard interface")) - } - if err := netlink.LinkSetUp(link); err != nil { - logrus.Warn(errors.Wrap(err, "failed to bring wireguard interface up")) - } - return nil -} - -func ConfigureForwarding(wgIface string, gatewayIface string, cidr string) error { - // Networking configuration (iptables) configuration - // to ensure that traffic from clients the wireguard interface - // is sent to the provided network interface - ipt, err := iptables.New() - if err != nil { - return errors.Wrap(err, "failed to init iptables") - } - logrus.Infof("iptables rule - accept forwarding traffic from %s to interface %s", gatewayIface, wgIface) - if err := ipt.AppendUnique("filter", "FORWARD", "-i", gatewayIface, "-o", wgIface, "-j", "ACCEPT"); err != nil { - return errors.Wrap(err, "failed to set ip tables rule") - } - logrus.Infof("iptables rule - accept forwarding traffic from %s to interface %s", wgIface, gatewayIface) - if err := ipt.AppendUnique("filter", "FORWARD", "-i", wgIface, "-o", gatewayIface, "-j", "ACCEPT"); err != nil { - return errors.Wrap(err, "failed to set ip tables rule") - } - logrus.Infof("iptables rule - masquerade traffic from %s to interface %s", cidr, gatewayIface) - if err := ipt.AppendUnique("nat", "POSTROUTING", "-s", cidr, "-o", gatewayIface, "-j", "MASQUERADE"); err != nil { - return errors.Wrap(err, "failed to set ip tables rule") - } - return nil -} - -func MustParseCIDR(cidr string) (net.IP, *net.IPNet) { - ip, ipnet, err := net.ParseCIDR(cidr) - if err != nil { - panic(err) - } - return ip, ipnet -} - -func MustParseIP(ip string) net.IP { - netip, _ := MustParseCIDR(fmt.Sprintf("%s/32", ip)) - return netip -} - -func nextIP(ip net.IP) net.IP { - next := make([]byte, len(ip)) - copy(next, ip) - for j := len(next) - 1; j >= 0; j-- { - next[j]++ - if next[j] > 0 { - break - } - } - return next -} diff --git a/internal/services/server_service.go b/internal/services/server_service.go index 6b2e773..e70dd3e 100644 --- a/internal/services/server_service.go +++ b/internal/services/server_service.go @@ -1,10 +1,11 @@ package services import ( + "github.com/place1/wg-access-server/internal/network" "context" - "github.com/place1/wg-access-server/internal/auth/authsession" "github.com/place1/wg-access-server/internal/config" + "github.com/place1/wg-access-server/pkg/authnz/authsession" "github.com/place1/wg-access-server/proto/proto" "github.com/place1/wg-embed/pkg/wgembed" "github.com/sirupsen/logrus" @@ -17,7 +18,8 @@ type ServerService struct { } func (s *ServerService) Info(ctx context.Context, req *proto.InfoReq) (*proto.InfoRes, error) { - if _, err := authsession.CurrentUser(ctx); err != nil { + user, err := authsession.CurrentUser(ctx) + if err != nil { return nil, status.Errorf(codes.PermissionDenied, "not authenticated") } @@ -28,9 +30,11 @@ func (s *ServerService) Info(ctx context.Context, req *proto.InfoReq) (*proto.In } return &proto.InfoRes{ - Host: stringValue(s.Config.WireGuard.ExternalHost), - PublicKey: publicKey, - Port: int32(s.Config.WireGuard.Port), - HostVpnIp: ServerVPNIP(s.Config.VPN.CIDR).IP.String(), + Host: stringValue(s.Config.WireGuard.ExternalHost), + PublicKey: publicKey, + Port: int32(s.Config.WireGuard.Port), + HostVpnIp: network.ServerVPNIP(s.Config.VPN.CIDR).IP.String(), + MetadataEnabled: !s.Config.DisableMetadata, + IsAdmin: user.Claims.Contains("admin"), }, nil } diff --git a/internal/storage/contracts.go b/internal/storage/contracts.go index 26ae9f3..eea937d 100644 --- a/internal/storage/contracts.go +++ b/internal/storage/contracts.go @@ -17,4 +17,16 @@ type Device struct { PublicKey string `json:"publicKey"` Address string `json:"address"` CreatedAt time.Time `json:"createdAt"` + + /** + * Metadata fields below. + * All metadata tracking can be disabled + * from the config file. + */ + + // metadata about the device during the current session + LastHandshakeTime *time.Time `json:"lastHandshakeTime"` + ReceiveBytes int64 `json:"receivedBytes"` + TransmitBytes int64 `json:"transmitBytes"` + Endpoint string `json:"endpoint"` } diff --git a/internal/storage/disk.go b/internal/storage/disk.go index d080648..efcae31 100644 --- a/internal/storage/disk.go +++ b/internal/storage/disk.go @@ -28,7 +28,7 @@ func NewDiskStorage(directory string) *DiskStorage { func (s *DiskStorage) Save(key string, device *Device) error { path := s.deviceFilePath(key) - logrus.Infof("saving new device %s", path) + logrus.Debugf("saving device %s", path) bytes, err := json.Marshal(device) if err != nil { return errors.Wrap(err, "failed to marshal device") diff --git a/main.go b/main.go index da02ac2..be80cb6 100644 --- a/main.go +++ b/main.go @@ -2,8 +2,11 @@ package main import ( "crypto/rand" + "fmt" "math" "net/http" + "net/url" + "os" "runtime/debug" "github.com/improbable-eng/grpc-web/go/grpcweb" @@ -13,15 +16,18 @@ import ( "github.com/place1/wg-embed/pkg/wgembed" "github.com/pkg/errors" - "github.com/place1/wg-access-server/internal/auth" - "github.com/place1/wg-access-server/internal/auth/authsession" "github.com/place1/wg-access-server/internal/config" "github.com/place1/wg-access-server/internal/devices" "github.com/place1/wg-access-server/internal/dnsproxy" + "github.com/place1/wg-access-server/internal/network" "github.com/place1/wg-access-server/internal/services" "github.com/place1/wg-access-server/internal/storage" + "github.com/place1/wg-access-server/pkg/authnz" + "github.com/place1/wg-access-server/pkg/authnz/authsession" "github.com/sirupsen/logrus" + "net/http/httputil" + grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" grpc_logrus "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus" grpc_recovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery" @@ -32,7 +38,7 @@ func main() { conf := config.Read() // The server's IP within the VPN virtual network - vpnip := services.ServerVPNIP(conf.VPN.CIDR) + vpnip := network.ServerVPNIP(conf.VPN.CIDR) // WireGuard Server wg, err := wgembed.New(conf.WireGuard.InterfaceName) @@ -49,18 +55,9 @@ func main() { }, }) - // Networking configuration - if err := services.ConfigureRouting(conf.WireGuard.InterfaceName, conf.VPN.CIDR); err != nil { + if err := network.ConfigureForwarding(conf.WireGuard.InterfaceName, conf.VPN.GatewayInterface, conf.VPN.CIDR, *conf.VPN.Rules); err != nil { logrus.Fatal(err) } - if conf.VPN.GatewayInterface != "" { - logrus.Infof("vpn gateway interface is %s", conf.VPN.GatewayInterface) - if err := services.ConfigureForwarding(conf.WireGuard.InterfaceName, conf.VPN.GatewayInterface, conf.VPN.CIDR); err != nil { - logrus.Fatal(err) - } - } else { - logrus.Warn("VPN.GatewayInterface is not configured - vpn clients will not have access to the internet") - } // DNS Server dns, err := dnsproxy.New(conf.DNS.Upstream) @@ -80,20 +77,28 @@ func main() { // Services deviceManager := devices.New(wg.Name(), storageDriver, conf.VPN.CIDR) - if err := deviceManager.Sync(); err != nil { + if err := deviceManager.StartSync(conf.DisableMetadata); err != nil { logrus.Fatal(errors.Wrap(err, "failed to sync")) } // Router router := mux.NewRouter() - router.PathPrefix("/").Handler(http.FileServer(http.Dir("website/build"))) + + // if the built website exists, serve that + // otherwise proxy to a local webpack development server + if _, err := os.Stat("website/build"); os.IsNotExist(err) { + u, _ := url.Parse("http://localhost:3000") + router.NotFoundHandler = httputil.NewSingleHostReverseProxy(u) + } else { + router.PathPrefix("/").Handler(http.FileServer(http.Dir("website/build"))) + } // GRPC Server server := grpc.NewServer([]grpc.ServerOption{ grpc.MaxRecvMsgSize(int(1 * math.Pow(2, 20))), // 1MB grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer( - grpc_recovery.UnaryServerInterceptor(), grpc_logrus.UnaryServerInterceptor(logrus.NewEntry(logrus.StandardLogger())), + grpc_recovery.UnaryServerInterceptor(), )), }...) proto.RegisterServerServer(server, &services.ServerService{ @@ -121,8 +126,13 @@ func main() { } }) - if conf.IsAuthEnabled() { - handler = auth.New(conf.Auth).Wrap(handler) + if conf.Auth.IsEnabled() { + handler = authnz.New(conf.Auth, func(user *authsession.Identity) error { + if user.Subject == conf.AdminSubject { + user.Claims.Add("admin", "true") + } + return nil + }).Wrap(handler) } else { base := handler handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -134,11 +144,18 @@ func main() { }) } + publicRouter := mux.NewRouter() + publicRouter.Handle("/health", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + fmt.Fprintf(w, "ok") + })).Methods("GET") + publicRouter.NotFoundHandler = handler + // Listen address := "0.0.0.0:8000" srv := &http.Server{ Addr: address, - Handler: handler, + Handler: publicRouter, } // Start Web server diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..344ea9c --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,32 @@ +site_name: wg-access-server +strict: true +repo_name: place1/wg-access-server +repo_url: https://github.com/place1/wg-access-server +site_url: https://place1.github.io/wg-access-server + +theme: + name: material + include_sidebar: true + logo: + icon: cloud_queue # globe icon + feature: + tabs: false + palette: + primary: light blue + accent: teal + +markdown_extensions: + - admonition + - codehilite + - pymdownx.inlinehilite + - pymdownx.superfences + - markdown_include.include: + base_path: docs + - meta + - pymdownx.tasklist: + custom_checkbox: true + - toc: + permalink: " ΒΆ" + +plugins: + - search diff --git a/internal/auth/authconfig/authconfig.go b/pkg/authnz/authconfig/authconfig.go similarity index 76% rename from internal/auth/authconfig/authconfig.go rename to pkg/authnz/authconfig/authconfig.go index 38b3056..c8d1743 100644 --- a/internal/auth/authconfig/authconfig.go +++ b/pkg/authnz/authconfig/authconfig.go @@ -1,7 +1,7 @@ package authconfig import ( - "github.com/place1/wg-access-server/internal/auth/authruntime" + "github.com/place1/wg-access-server/pkg/authnz/authruntime" ) type AuthConfig struct { @@ -10,6 +10,10 @@ type AuthConfig struct { Basic *BasicAuthConfig `yaml:"basic"` } +func (c *AuthConfig) IsEnabled() bool { + return c.OIDC != nil || c.Gitlab != nil || c.Basic != nil +} + func (c *AuthConfig) Providers() []*authruntime.Provider { providers := []*authruntime.Provider{} diff --git a/internal/auth/authconfig/basic.go b/pkg/authnz/authconfig/basic.go similarity index 88% rename from internal/auth/authconfig/basic.go rename to pkg/authnz/authconfig/basic.go index 00d1f50..63d1e5d 100644 --- a/internal/auth/authconfig/basic.go +++ b/pkg/authnz/authconfig/basic.go @@ -5,8 +5,8 @@ import ( "net/http" "strings" - "github.com/place1/wg-access-server/internal/auth/authruntime" - "github.com/place1/wg-access-server/internal/auth/authsession" + "github.com/place1/wg-access-server/pkg/authnz/authruntime" + "github.com/place1/wg-access-server/pkg/authnz/authsession" "github.com/tg123/go-htpasswd" ) @@ -43,9 +43,13 @@ func basicAuthLogin(c *BasicAuthConfig, runtime *authruntime.ProviderRuntime) ht Subject: u, }, }) + runtime.Done(w, r) } - runtime.Done(w, r) + w.Header().Set("WWW-Authenticate", `Basic realm="site"`) + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprintln(w, "unauthorized") + return } } diff --git a/internal/auth/authconfig/gitlab.go b/pkg/authnz/authconfig/gitlab.go similarity index 90% rename from internal/auth/authconfig/gitlab.go rename to pkg/authnz/authconfig/gitlab.go index 51c2a3d..e1223ef 100644 --- a/internal/auth/authconfig/gitlab.go +++ b/pkg/authnz/authconfig/gitlab.go @@ -1,6 +1,6 @@ package authconfig -import "github.com/place1/wg-access-server/internal/auth/authruntime" +import "github.com/place1/wg-access-server/pkg/authnz/authruntime" type GitlabConfig struct { Name string `yaml:"name"` diff --git a/internal/auth/authconfig/oidc.go b/pkg/authnz/authconfig/oidc.go similarity index 94% rename from internal/auth/authconfig/oidc.go rename to pkg/authnz/authconfig/oidc.go index 098277c..e340209 100644 --- a/internal/auth/authconfig/oidc.go +++ b/pkg/authnz/authconfig/oidc.go @@ -10,9 +10,9 @@ import ( "github.com/coreos/go-oidc" "github.com/gorilla/mux" "github.com/pkg/errors" - "github.com/place1/wg-access-server/internal/auth/authruntime" - "github.com/place1/wg-access-server/internal/auth/authsession" - "github.com/place1/wg-access-server/internal/auth/authutil" + "github.com/place1/wg-access-server/pkg/authnz/authruntime" + "github.com/place1/wg-access-server/pkg/authnz/authsession" + "github.com/place1/wg-access-server/pkg/authnz/authutil" "github.com/sirupsen/logrus" "golang.org/x/oauth2" ) diff --git a/internal/auth/authruntime/runtime.go b/pkg/authnz/authruntime/runtime.go similarity index 94% rename from internal/auth/authruntime/runtime.go rename to pkg/authnz/authruntime/runtime.go index 1a82abf..ff9eee4 100644 --- a/internal/auth/authruntime/runtime.go +++ b/pkg/authnz/authruntime/runtime.go @@ -5,7 +5,7 @@ import ( "github.com/gorilla/mux" "github.com/gorilla/sessions" - "github.com/place1/wg-access-server/internal/auth/authsession" + "github.com/place1/wg-access-server/pkg/authnz/authsession" ) type Provider struct { diff --git a/pkg/authnz/authsession/claims.go b/pkg/authnz/authsession/claims.go new file mode 100644 index 0000000..9e3be70 --- /dev/null +++ b/pkg/authnz/authsession/claims.go @@ -0,0 +1,35 @@ +package authsession + +type claim struct { + Name string + Value string +} + +type Claims []claim + +func (c *Claims) Add(name string, value string) { + *c = append(*c, claim{ + Name: name, + Value: value, + }) +} + +func (c *Claims) Contains(claim string) bool { + for _, curr := range *c { + if curr.Name == claim { + return true + } + } + return false +} + +func (c *Claims) Has(claim string, value string) bool { + for _, curr := range *c { + if curr.Name == claim { + if curr.Value == value { + return true + } + } + } + return false +} diff --git a/pkg/authnz/authsession/identity.go b/pkg/authnz/authsession/identity.go new file mode 100644 index 0000000..f8bcfb8 --- /dev/null +++ b/pkg/authnz/authsession/identity.go @@ -0,0 +1,6 @@ +package authsession + +type Identity struct { + Subject string + Claims Claims +} diff --git a/pkg/authnz/authsession/middleware.go b/pkg/authnz/authsession/middleware.go new file mode 100644 index 0000000..4ea0438 --- /dev/null +++ b/pkg/authnz/authsession/middleware.go @@ -0,0 +1,3 @@ +package authsession + +type ClaimsMiddleware func(user *Identity) error diff --git a/internal/auth/authsession/session.go b/pkg/authnz/authsession/session.go similarity index 97% rename from internal/auth/authsession/session.go rename to pkg/authnz/authsession/session.go index b079373..892f870 100644 --- a/internal/auth/authsession/session.go +++ b/pkg/authnz/authsession/session.go @@ -15,10 +15,6 @@ type AuthSession struct { Identity *Identity } -type Identity struct { - Subject string -} - type authSessionKey string var sessionKey authSessionKey = "auth-session" diff --git a/internal/auth/authtemplates/login.go b/pkg/authnz/authtemplates/login.go similarity index 97% rename from internal/auth/authtemplates/login.go rename to pkg/authnz/authtemplates/login.go index b70aa98..2e70c86 100644 --- a/internal/auth/authtemplates/login.go +++ b/pkg/authnz/authtemplates/login.go @@ -4,7 +4,7 @@ import ( "html/template" "io" - "github.com/place1/wg-access-server/internal/auth/authruntime" + "github.com/place1/wg-access-server/pkg/authnz/authruntime" ) type LoginPage struct { diff --git a/internal/auth/authutil/random.go b/pkg/authnz/authutil/random.go similarity index 100% rename from internal/auth/authutil/random.go rename to pkg/authnz/authutil/random.go diff --git a/internal/auth/router.go b/pkg/authnz/router.go similarity index 64% rename from internal/auth/router.go rename to pkg/authnz/router.go index a886510..0105141 100644 --- a/internal/auth/router.go +++ b/pkg/authnz/router.go @@ -1,26 +1,30 @@ -package auth +package authnz import ( "fmt" "net/http" "strconv" - "github.com/place1/wg-access-server/internal/auth/authconfig" - "github.com/place1/wg-access-server/internal/auth/authruntime" - "github.com/place1/wg-access-server/internal/auth/authsession" - "github.com/place1/wg-access-server/internal/auth/authtemplates" - "github.com/place1/wg-access-server/internal/auth/authutil" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + + "github.com/place1/wg-access-server/pkg/authnz/authconfig" + "github.com/place1/wg-access-server/pkg/authnz/authruntime" + "github.com/place1/wg-access-server/pkg/authnz/authsession" + "github.com/place1/wg-access-server/pkg/authnz/authtemplates" + "github.com/place1/wg-access-server/pkg/authnz/authutil" "github.com/gorilla/mux" "github.com/gorilla/sessions" ) type AuthMiddleware struct { - config *authconfig.AuthConfig + config authconfig.AuthConfig + claimsMiddleware authsession.ClaimsMiddleware } -func New(config *authconfig.AuthConfig) *AuthMiddleware { - return &AuthMiddleware{config} +func New(config authconfig.AuthConfig, claimsMiddleware authsession.ClaimsMiddleware) *AuthMiddleware { + return &AuthMiddleware{config, claimsMiddleware} } func (m *AuthMiddleware) Wrap(next http.Handler) http.Handler { @@ -61,6 +65,13 @@ func (m *AuthMiddleware) Wrap(next http.Handler) http.Handler { router.PathPrefix("/").Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if s, err := runtime.GetSession(r); err == nil { + if m.claimsMiddleware != nil { + if err := m.claimsMiddleware(s.Identity); err != nil { + logrus.Error(errors.Wrap(err, "authz middleware failure")) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + } next.ServeHTTP(w, r.WithContext(authsession.SetIdentityCtx(r.Context(), s))) } else { next.ServeHTTP(w, r) diff --git a/proto/devices.proto b/proto/devices.proto index 17bada3..a46cadc 100644 --- a/proto/devices.proto +++ b/proto/devices.proto @@ -9,6 +9,9 @@ service Devices { rpc AddDevice(AddDeviceReq) returns (Device) {} rpc ListDevices(ListDevicesReq) returns (ListDevicesRes) {} rpc DeleteDevice(DeleteDeviceReq) returns (google.protobuf.Empty) {} + + // admin only + rpc ListAllDevices(ListAllDevicesReq) returns (ListAllDevicesRes) {} } message Device { @@ -17,6 +20,11 @@ message Device { string public_key = 3; string address = 4; google.protobuf.Timestamp created_at = 5; + bool connected = 6; + google.protobuf.Timestamp last_handshake_time = 7; + int64 receive_bytes = 8; + int64 transmit_bytes = 9; + string endpoint = 10; } message AddDeviceReq { @@ -35,3 +43,11 @@ message ListDevicesRes { message DeleteDeviceReq { string name = 1; } + +message ListAllDevicesReq { + +} + +message ListAllDevicesRes { + repeated Device items = 1; +} diff --git a/proto/proto/devices.pb.go b/proto/proto/devices.pb.go index 9fbe763..fd1dc4d 100644 --- a/proto/proto/devices.pb.go +++ b/proto/proto/devices.pb.go @@ -32,6 +32,11 @@ type Device struct { PublicKey string `protobuf:"bytes,3,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"` Address string `protobuf:"bytes,4,opt,name=address,proto3" json:"address,omitempty"` CreatedAt *timestamp.Timestamp `protobuf:"bytes,5,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + Connected bool `protobuf:"varint,6,opt,name=connected,proto3" json:"connected,omitempty"` + LastHandshakeTime *timestamp.Timestamp `protobuf:"bytes,7,opt,name=last_handshake_time,json=lastHandshakeTime,proto3" json:"last_handshake_time,omitempty"` + ReceiveBytes int64 `protobuf:"varint,8,opt,name=receive_bytes,json=receiveBytes,proto3" json:"receive_bytes,omitempty"` + TransmitBytes int64 `protobuf:"varint,9,opt,name=transmit_bytes,json=transmitBytes,proto3" json:"transmit_bytes,omitempty"` + Endpoint string `protobuf:"bytes,10,opt,name=endpoint,proto3" json:"endpoint,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` @@ -97,6 +102,41 @@ func (m *Device) GetCreatedAt() *timestamp.Timestamp { return nil } +func (m *Device) GetConnected() bool { + if m != nil { + return m.Connected + } + return false +} + +func (m *Device) GetLastHandshakeTime() *timestamp.Timestamp { + if m != nil { + return m.LastHandshakeTime + } + return nil +} + +func (m *Device) GetReceiveBytes() int64 { + if m != nil { + return m.ReceiveBytes + } + return 0 +} + +func (m *Device) GetTransmitBytes() int64 { + if m != nil { + return m.TransmitBytes + } + return 0 +} + +func (m *Device) GetEndpoint() string { + if m != nil { + return m.Endpoint + } + return "" +} + type AddDeviceReq struct { Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` PublicKey string `protobuf:"bytes,2,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"` @@ -253,39 +293,120 @@ func (m *DeleteDeviceReq) GetName() string { return "" } +type ListAllDevicesReq struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ListAllDevicesReq) Reset() { *m = ListAllDevicesReq{} } +func (m *ListAllDevicesReq) String() string { return proto.CompactTextString(m) } +func (*ListAllDevicesReq) ProtoMessage() {} +func (*ListAllDevicesReq) Descriptor() ([]byte, []int) { + return fileDescriptor_6d27ec3f2c0e2043, []int{5} +} + +func (m *ListAllDevicesReq) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ListAllDevicesReq.Unmarshal(m, b) +} +func (m *ListAllDevicesReq) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ListAllDevicesReq.Marshal(b, m, deterministic) +} +func (m *ListAllDevicesReq) XXX_Merge(src proto.Message) { + xxx_messageInfo_ListAllDevicesReq.Merge(m, src) +} +func (m *ListAllDevicesReq) XXX_Size() int { + return xxx_messageInfo_ListAllDevicesReq.Size(m) +} +func (m *ListAllDevicesReq) XXX_DiscardUnknown() { + xxx_messageInfo_ListAllDevicesReq.DiscardUnknown(m) +} + +var xxx_messageInfo_ListAllDevicesReq proto.InternalMessageInfo + +type ListAllDevicesRes struct { + Items []*Device `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ListAllDevicesRes) Reset() { *m = ListAllDevicesRes{} } +func (m *ListAllDevicesRes) String() string { return proto.CompactTextString(m) } +func (*ListAllDevicesRes) ProtoMessage() {} +func (*ListAllDevicesRes) Descriptor() ([]byte, []int) { + return fileDescriptor_6d27ec3f2c0e2043, []int{6} +} + +func (m *ListAllDevicesRes) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ListAllDevicesRes.Unmarshal(m, b) +} +func (m *ListAllDevicesRes) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ListAllDevicesRes.Marshal(b, m, deterministic) +} +func (m *ListAllDevicesRes) XXX_Merge(src proto.Message) { + xxx_messageInfo_ListAllDevicesRes.Merge(m, src) +} +func (m *ListAllDevicesRes) XXX_Size() int { + return xxx_messageInfo_ListAllDevicesRes.Size(m) +} +func (m *ListAllDevicesRes) XXX_DiscardUnknown() { + xxx_messageInfo_ListAllDevicesRes.DiscardUnknown(m) +} + +var xxx_messageInfo_ListAllDevicesRes proto.InternalMessageInfo + +func (m *ListAllDevicesRes) GetItems() []*Device { + if m != nil { + return m.Items + } + return nil +} + func init() { proto.RegisterType((*Device)(nil), "proto.Device") proto.RegisterType((*AddDeviceReq)(nil), "proto.AddDeviceReq") proto.RegisterType((*ListDevicesReq)(nil), "proto.ListDevicesReq") proto.RegisterType((*ListDevicesRes)(nil), "proto.ListDevicesRes") proto.RegisterType((*DeleteDeviceReq)(nil), "proto.DeleteDeviceReq") + proto.RegisterType((*ListAllDevicesReq)(nil), "proto.ListAllDevicesReq") + proto.RegisterType((*ListAllDevicesRes)(nil), "proto.ListAllDevicesRes") } func init() { proto.RegisterFile("devices.proto", fileDescriptor_6d27ec3f2c0e2043) } var fileDescriptor_6d27ec3f2c0e2043 = []byte{ - // 332 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x92, 0xc1, 0x4e, 0xfa, 0x40, - 0x10, 0xc6, 0x29, 0x50, 0x48, 0x07, 0xf8, 0xff, 0xcd, 0xa8, 0x64, 0xb3, 0xc6, 0x48, 0xd6, 0x98, - 0x70, 0x2a, 0x11, 0xe3, 0xc1, 0x83, 0x89, 0x24, 0x78, 0xd2, 0x53, 0xe3, 0x9d, 0x14, 0x76, 0x24, - 0x8d, 0x94, 0x96, 0xee, 0xa2, 0xe1, 0x85, 0x7c, 0x0b, 0xdf, 0xcd, 0xb8, 0x5b, 0x08, 0xad, 0xc4, - 0xd3, 0xee, 0x7c, 0xdf, 0xe4, 0x9b, 0xdf, 0x6c, 0x16, 0x3a, 0x92, 0xde, 0xa3, 0x19, 0x29, 0x3f, - 0xcd, 0x12, 0x9d, 0xa0, 0x6b, 0x0e, 0x7e, 0x31, 0x4f, 0x92, 0xf9, 0x82, 0x06, 0xa6, 0x9a, 0xae, - 0x5f, 0x07, 0x3a, 0x8a, 0x49, 0xe9, 0x30, 0x4e, 0x6d, 0x1f, 0x3f, 0x2b, 0x37, 0x50, 0x9c, 0xea, - 0x8d, 0x35, 0xc5, 0xa7, 0x03, 0x8d, 0xb1, 0x89, 0x45, 0x84, 0xfa, 0x32, 0x8c, 0x89, 0x39, 0x3d, - 0xa7, 0xef, 0x05, 0xe6, 0x8e, 0x27, 0xe0, 0x26, 0x1f, 0x4b, 0xca, 0x58, 0xd5, 0x88, 0xb6, 0xc0, - 0x73, 0x80, 0x74, 0x3d, 0x5d, 0x44, 0xb3, 0xc9, 0x1b, 0x6d, 0x58, 0xcd, 0x58, 0x9e, 0x55, 0x9e, - 0x68, 0x83, 0x0c, 0x9a, 0xa1, 0x94, 0x19, 0x29, 0xc5, 0xea, 0xc6, 0xdb, 0x96, 0x78, 0x07, 0x30, - 0xcb, 0x28, 0xd4, 0x24, 0x27, 0xa1, 0x66, 0x6e, 0xcf, 0xe9, 0xb7, 0x86, 0xdc, 0xb7, 0x7c, 0xfe, - 0x96, 0xcf, 0x7f, 0xd9, 0x2e, 0x10, 0x78, 0x79, 0xf7, 0x48, 0x8b, 0x11, 0xb4, 0x47, 0x52, 0x5a, - 0xd4, 0x80, 0x56, 0x07, 0x69, 0x8b, 0x5c, 0xd5, 0x12, 0x97, 0x38, 0x82, 0x7f, 0xcf, 0x91, 0xd2, - 0x36, 0x43, 0x05, 0xb4, 0x12, 0xb7, 0x25, 0x45, 0xe1, 0x25, 0xb8, 0x91, 0xa6, 0x58, 0x31, 0xa7, - 0x57, 0xeb, 0xb7, 0x86, 0x1d, 0x4b, 0xe5, 0xe7, 0x73, 0xad, 0x27, 0xae, 0xe0, 0xff, 0x98, 0x16, - 0xa4, 0xe9, 0x4f, 0x9c, 0xe1, 0x97, 0x03, 0xcd, 0x3c, 0x1a, 0xaf, 0xc1, 0xdb, 0xe1, 0xe3, 0x71, - 0x9e, 0xba, 0xbf, 0x10, 0x2f, 0x8e, 0x12, 0x15, 0xbc, 0x87, 0xd6, 0x1e, 0x1c, 0x9e, 0xe6, 0x7e, - 0x71, 0x05, 0x7e, 0x50, 0x56, 0xa2, 0x82, 0x0f, 0xd0, 0xde, 0x87, 0xc4, 0xee, 0x2e, 0xbf, 0x40, - 0xce, 0xbb, 0xbf, 0xde, 0xff, 0xf1, 0xe7, 0x7f, 0x88, 0xca, 0xb4, 0x61, 0x94, 0x9b, 0xef, 0x00, - 0x00, 0x00, 0xff, 0xff, 0x55, 0x8e, 0x8a, 0x9f, 0x78, 0x02, 0x00, 0x00, + // 467 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x93, 0xcb, 0x6f, 0xd3, 0x40, + 0x10, 0xc6, 0xe3, 0xa4, 0x79, 0x78, 0x92, 0x14, 0x3a, 0x81, 0x6a, 0x65, 0x40, 0x58, 0x5b, 0x55, + 0xf2, 0xc9, 0x15, 0x41, 0x48, 0x70, 0x40, 0x22, 0xa8, 0x20, 0x04, 0x9c, 0x2c, 0xee, 0x96, 0x63, + 0x0f, 0xad, 0x55, 0xbf, 0xea, 0xdd, 0x16, 0xe5, 0x0f, 0xe0, 0x7f, 0xe6, 0x88, 0xbc, 0x6b, 0xa7, + 0x76, 0x1a, 0x1e, 0x27, 0x7b, 0xbe, 0xef, 0xdb, 0x99, 0x9d, 0x9f, 0x6c, 0x98, 0x47, 0x74, 0x1b, + 0x87, 0x24, 0xdc, 0xa2, 0xcc, 0x65, 0x8e, 0x43, 0xf5, 0xb0, 0x9e, 0x5f, 0xe4, 0xf9, 0x45, 0x42, + 0x67, 0xaa, 0x5a, 0xdf, 0x7c, 0x3f, 0x93, 0x71, 0x4a, 0x42, 0x06, 0x69, 0xa1, 0x73, 0xd6, 0x93, + 0xdd, 0x00, 0xa5, 0x85, 0xdc, 0x68, 0x93, 0xff, 0xea, 0xc3, 0xe8, 0x5c, 0xb5, 0x45, 0x84, 0x83, + 0x2c, 0x48, 0x89, 0x19, 0xb6, 0xe1, 0x98, 0x9e, 0x7a, 0xc7, 0x47, 0x30, 0xcc, 0x7f, 0x64, 0x54, + 0xb2, 0xbe, 0x12, 0x75, 0x81, 0xcf, 0x00, 0x8a, 0x9b, 0x75, 0x12, 0x87, 0xfe, 0x15, 0x6d, 0xd8, + 0x40, 0x59, 0xa6, 0x56, 0xbe, 0xd0, 0x06, 0x19, 0x8c, 0x83, 0x28, 0x2a, 0x49, 0x08, 0x76, 0xa0, + 0xbc, 0xa6, 0xc4, 0x37, 0x00, 0x61, 0x49, 0x81, 0xa4, 0xc8, 0x0f, 0x24, 0x1b, 0xda, 0x86, 0x33, + 0x5d, 0x5a, 0xae, 0xbe, 0x9f, 0xdb, 0xdc, 0xcf, 0xfd, 0xd6, 0x2c, 0xe0, 0x99, 0x75, 0x7a, 0x25, + 0xf1, 0x29, 0x98, 0x61, 0x9e, 0x65, 0x14, 0x4a, 0x8a, 0xd8, 0xc8, 0x36, 0x9c, 0x89, 0x77, 0x27, + 0xe0, 0x67, 0x58, 0x24, 0x81, 0x90, 0xfe, 0x65, 0x90, 0x45, 0xe2, 0x32, 0xb8, 0x22, 0xbf, 0xa2, + 0xc0, 0xc6, 0xff, 0x9c, 0x70, 0x54, 0x1d, 0xfb, 0xd4, 0x9c, 0xaa, 0x74, 0x3c, 0x81, 0x79, 0x49, + 0x21, 0xc5, 0xb7, 0xe4, 0xaf, 0x37, 0x92, 0x04, 0x9b, 0xd8, 0x86, 0x33, 0xf0, 0x66, 0xb5, 0xf8, + 0xbe, 0xd2, 0xf0, 0x14, 0x0e, 0x65, 0x19, 0x64, 0x22, 0x8d, 0x65, 0x9d, 0x32, 0x55, 0x6a, 0xde, + 0xa8, 0x3a, 0x66, 0xc1, 0x84, 0xb2, 0xa8, 0xc8, 0xe3, 0x4c, 0x32, 0x50, 0x2c, 0xb6, 0x35, 0x5f, + 0xc1, 0x6c, 0x15, 0x45, 0x1a, 0xbe, 0x47, 0xd7, 0x7b, 0xf9, 0x77, 0x49, 0xf7, 0x77, 0x48, 0xf3, + 0x87, 0x70, 0xf8, 0x35, 0x16, 0x52, 0xf7, 0x10, 0x1e, 0x5d, 0xf3, 0x57, 0x3b, 0x8a, 0xc0, 0x13, + 0x18, 0xc6, 0x92, 0x52, 0xc1, 0x0c, 0x7b, 0xe0, 0x4c, 0x97, 0x73, 0x4d, 0xc1, 0xad, 0xe7, 0x6a, + 0x8f, 0x9f, 0xc2, 0x83, 0x73, 0x4a, 0x48, 0xd2, 0x5f, 0xaf, 0xc3, 0x17, 0x70, 0x54, 0x75, 0x5f, + 0x25, 0x49, 0x6b, 0xe4, 0xeb, 0xfb, 0xe2, 0xff, 0x4d, 0x5d, 0xfe, 0xec, 0xc3, 0xb8, 0x3e, 0x83, + 0x2f, 0xc0, 0xdc, 0xd2, 0xc0, 0x45, 0x1d, 0x6f, 0xf3, 0xb1, 0xba, 0x3d, 0x78, 0x0f, 0xdf, 0xc2, + 0xb4, 0xb5, 0x2b, 0x3e, 0xae, 0xfd, 0x2e, 0x11, 0x6b, 0xaf, 0x2c, 0x78, 0x0f, 0xdf, 0xc1, 0xac, + 0xbd, 0x33, 0x1e, 0x6f, 0xfb, 0x77, 0x40, 0x58, 0xc7, 0xf7, 0x3e, 0x9f, 0x0f, 0xd5, 0x0f, 0xc4, + 0x7b, 0xf8, 0x51, 0xc3, 0xbe, 0xdb, 0x1c, 0x59, 0x6b, 0x58, 0x87, 0x92, 0xf5, 0x27, 0x47, 0xf0, + 0xde, 0x7a, 0xa4, 0xac, 0x97, 0xbf, 0x03, 0x00, 0x00, 0xff, 0xff, 0x2f, 0x41, 0xb7, 0x19, 0xe1, + 0x03, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -303,6 +424,8 @@ type DevicesClient interface { AddDevice(ctx context.Context, in *AddDeviceReq, opts ...grpc.CallOption) (*Device, error) ListDevices(ctx context.Context, in *ListDevicesReq, opts ...grpc.CallOption) (*ListDevicesRes, error) DeleteDevice(ctx context.Context, in *DeleteDeviceReq, opts ...grpc.CallOption) (*empty.Empty, error) + // admin only + ListAllDevices(ctx context.Context, in *ListAllDevicesReq, opts ...grpc.CallOption) (*ListAllDevicesRes, error) } type devicesClient struct { @@ -340,11 +463,22 @@ func (c *devicesClient) DeleteDevice(ctx context.Context, in *DeleteDeviceReq, o return out, nil } +func (c *devicesClient) ListAllDevices(ctx context.Context, in *ListAllDevicesReq, opts ...grpc.CallOption) (*ListAllDevicesRes, error) { + out := new(ListAllDevicesRes) + err := c.cc.Invoke(ctx, "/proto.Devices/ListAllDevices", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // DevicesServer is the server API for Devices service. type DevicesServer interface { AddDevice(context.Context, *AddDeviceReq) (*Device, error) ListDevices(context.Context, *ListDevicesReq) (*ListDevicesRes, error) DeleteDevice(context.Context, *DeleteDeviceReq) (*empty.Empty, error) + // admin only + ListAllDevices(context.Context, *ListAllDevicesReq) (*ListAllDevicesRes, error) } // UnimplementedDevicesServer can be embedded to have forward compatible implementations. @@ -360,6 +494,9 @@ func (*UnimplementedDevicesServer) ListDevices(ctx context.Context, req *ListDev func (*UnimplementedDevicesServer) DeleteDevice(ctx context.Context, req *DeleteDeviceReq) (*empty.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method DeleteDevice not implemented") } +func (*UnimplementedDevicesServer) ListAllDevices(ctx context.Context, req *ListAllDevicesReq) (*ListAllDevicesRes, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListAllDevices not implemented") +} func RegisterDevicesServer(s *grpc.Server, srv DevicesServer) { s.RegisterService(&_Devices_serviceDesc, srv) @@ -419,6 +556,24 @@ func _Devices_DeleteDevice_Handler(srv interface{}, ctx context.Context, dec fun return interceptor(ctx, in, info, handler) } +func _Devices_ListAllDevices_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListAllDevicesReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DevicesServer).ListAllDevices(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.Devices/ListAllDevices", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DevicesServer).ListAllDevices(ctx, req.(*ListAllDevicesReq)) + } + return interceptor(ctx, in, info, handler) +} + var _Devices_serviceDesc = grpc.ServiceDesc{ ServiceName: "proto.Devices", HandlerType: (*DevicesServer)(nil), @@ -435,6 +590,10 @@ var _Devices_serviceDesc = grpc.ServiceDesc{ MethodName: "DeleteDevice", Handler: _Devices_DeleteDevice_Handler, }, + { + MethodName: "ListAllDevices", + Handler: _Devices_ListAllDevices_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "devices.proto", diff --git a/proto/proto/server.pb.go b/proto/proto/server.pb.go index e6e78bf..1d11596 100644 --- a/proto/proto/server.pb.go +++ b/proto/proto/server.pb.go @@ -61,6 +61,8 @@ type InfoRes struct { Host *wrappers.StringValue `protobuf:"bytes,2,opt,name=host,proto3" json:"host,omitempty"` Port int32 `protobuf:"varint,3,opt,name=port,proto3" json:"port,omitempty"` HostVpnIp string `protobuf:"bytes,4,opt,name=host_vpn_ip,json=hostVpnIp,proto3" json:"host_vpn_ip,omitempty"` + MetadataEnabled bool `protobuf:"varint,5,opt,name=metadata_enabled,json=metadataEnabled,proto3" json:"metadata_enabled,omitempty"` + IsAdmin bool `protobuf:"varint,6,opt,name=is_admin,json=isAdmin,proto3" json:"is_admin,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` @@ -119,6 +121,20 @@ func (m *InfoRes) GetHostVpnIp() string { return "" } +func (m *InfoRes) GetMetadataEnabled() bool { + if m != nil { + return m.MetadataEnabled + } + return false +} + +func (m *InfoRes) GetIsAdmin() bool { + if m != nil { + return m.IsAdmin + } + return false +} + func init() { proto.RegisterType((*InfoReq)(nil), "proto.InfoReq") proto.RegisterType((*InfoRes)(nil), "proto.InfoRes") @@ -127,21 +143,24 @@ func init() { func init() { proto.RegisterFile("server.proto", fileDescriptor_ad098daeda4239f7) } var fileDescriptor_ad098daeda4239f7 = []byte{ - // 217 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x54, 0x8f, 0xc1, 0x4e, 0xc3, 0x30, - 0x10, 0x44, 0x31, 0xa4, 0x45, 0xd9, 0x22, 0x0e, 0x7b, 0xb2, 0x2a, 0xa8, 0xa2, 0x9c, 0x7c, 0x72, - 0x51, 0xf8, 0x8a, 0x8a, 0x9b, 0x2b, 0xf5, 0x1a, 0x35, 0x68, 0x1b, 0x22, 0x22, 0x7b, 0xb1, 0x9d, - 0xa2, 0xfe, 0x04, 0xdf, 0x8c, 0x62, 0xd3, 0x03, 0x27, 0xcf, 0x8c, 0x47, 0x7a, 0xb3, 0xf0, 0x10, - 0xc8, 0x9f, 0xc9, 0x6b, 0xf6, 0x2e, 0x3a, 0x5c, 0xa4, 0x67, 0xbd, 0xe9, 0x9d, 0xeb, 0x47, 0xda, - 0x26, 0xd7, 0x4d, 0xa7, 0xed, 0xb7, 0x3f, 0x32, 0x93, 0x0f, 0xb9, 0x56, 0x97, 0x70, 0xbf, 0xb3, - 0x27, 0x67, 0xe8, 0xab, 0xfe, 0x11, 0x57, 0x1d, 0xf0, 0x19, 0x80, 0xa7, 0x6e, 0x1c, 0xde, 0xdb, - 0x4f, 0xba, 0x48, 0x51, 0x09, 0x55, 0x9a, 0x32, 0x27, 0x6f, 0x74, 0xc1, 0x17, 0x28, 0x3e, 0x5c, - 0x88, 0xf2, 0xb6, 0x12, 0x6a, 0xd5, 0x3c, 0xe9, 0x0c, 0xd1, 0x57, 0x88, 0xde, 0x47, 0x3f, 0xd8, - 0xfe, 0x70, 0x1c, 0x27, 0x32, 0xa9, 0x89, 0x08, 0x05, 0x3b, 0x1f, 0xe5, 0x5d, 0x25, 0xd4, 0xc2, - 0x24, 0x8d, 0x1b, 0x58, 0xcd, 0x7f, 0xed, 0x99, 0x6d, 0x3b, 0xb0, 0x2c, 0x32, 0x65, 0x8e, 0x0e, - 0x6c, 0x77, 0xdc, 0x34, 0xb0, 0xdc, 0xa7, 0x93, 0x50, 0x41, 0x31, 0x2f, 0xc3, 0xc7, 0x8c, 0xd0, - 0x7f, 0x93, 0xd7, 0xff, 0x7d, 0xa8, 0x6f, 0xba, 0x65, 0x0a, 0x5e, 0x7f, 0x03, 0x00, 0x00, 0xff, - 0xff, 0x4d, 0xc6, 0xae, 0x09, 0x0d, 0x01, 0x00, 0x00, + // 263 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x54, 0x90, 0xb1, 0x4e, 0xf3, 0x30, + 0x1c, 0xc4, 0x3f, 0x7f, 0xa4, 0x69, 0xf3, 0x2f, 0x02, 0xe4, 0xc9, 0x54, 0x50, 0x45, 0x99, 0xc2, + 0x92, 0xa2, 0xf0, 0x04, 0x0c, 0x0c, 0x15, 0x5b, 0x2a, 0x75, 0x8d, 0x1c, 0xf2, 0x6f, 0xb0, 0x48, + 0x6d, 0x63, 0x3b, 0x45, 0x7d, 0x4f, 0x1e, 0x08, 0xc5, 0x26, 0x03, 0x93, 0xef, 0x7e, 0x3e, 0xe9, + 0x7c, 0x86, 0x4b, 0x8b, 0xe6, 0x84, 0xa6, 0xd0, 0x46, 0x39, 0x45, 0x67, 0xfe, 0x58, 0xad, 0x3b, + 0xa5, 0xba, 0x1e, 0x37, 0xde, 0x35, 0xc3, 0x61, 0xf3, 0x65, 0xb8, 0xd6, 0x68, 0x6c, 0x88, 0x65, + 0x09, 0xcc, 0xb7, 0xf2, 0xa0, 0x2a, 0xfc, 0xcc, 0xbe, 0xc9, 0xa4, 0x2d, 0xbd, 0x07, 0xd0, 0x43, + 0xd3, 0x8b, 0xb7, 0xfa, 0x03, 0xcf, 0x8c, 0xa4, 0x24, 0x4f, 0xaa, 0x24, 0x90, 0x57, 0x3c, 0xd3, + 0x47, 0x88, 0xde, 0x95, 0x75, 0xec, 0x7f, 0x4a, 0xf2, 0x65, 0x79, 0x57, 0x84, 0x92, 0x62, 0x2a, + 0x29, 0x76, 0xce, 0x08, 0xd9, 0xed, 0x79, 0x3f, 0x60, 0xe5, 0x93, 0x94, 0x42, 0xa4, 0x95, 0x71, + 0xec, 0x22, 0x25, 0xf9, 0xac, 0xf2, 0x9a, 0xae, 0x61, 0x39, 0xde, 0xd5, 0x27, 0x2d, 0x6b, 0xa1, + 0x59, 0x14, 0x5a, 0x46, 0xb4, 0xd7, 0x72, 0xab, 0xe9, 0x03, 0xdc, 0x1c, 0xd1, 0xf1, 0x96, 0x3b, + 0x5e, 0xa3, 0xe4, 0x4d, 0x8f, 0x2d, 0x9b, 0xa5, 0x24, 0x5f, 0x54, 0xd7, 0x13, 0x7f, 0x09, 0x98, + 0xde, 0xc2, 0x42, 0xd8, 0x9a, 0xb7, 0x47, 0x21, 0x59, 0xec, 0x23, 0x73, 0x61, 0x9f, 0x47, 0x5b, + 0x96, 0x10, 0xef, 0xfc, 0xc7, 0xd0, 0x1c, 0xa2, 0x71, 0x1f, 0xbd, 0x0a, 0x0f, 0x2d, 0x7e, 0x87, + 0xaf, 0xfe, 0x7a, 0x9b, 0xfd, 0x6b, 0x62, 0x0f, 0x9e, 0x7e, 0x02, 0x00, 0x00, 0xff, 0xff, 0x55, + 0xd5, 0x11, 0x8a, 0x53, 0x01, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. diff --git a/proto/server.proto b/proto/server.proto index ab97418..4b260e8 100644 --- a/proto/server.proto +++ b/proto/server.proto @@ -17,4 +17,6 @@ message InfoRes { google.protobuf.StringValue host = 2; int32 port = 3; string host_vpn_ip = 4; + bool metadata_enabled = 5; + bool is_admin = 6; } diff --git a/publish.py b/publish.py index 6130c94..8e9d0cc 100755 --- a/publish.py +++ b/publish.py @@ -4,41 +4,46 @@ import subprocess import json import yaml +def is_release_candidate(version): + return '-rc' in version + # print the latest tags so we don't have to google our own # image to check :P r = urllib.request.urlopen('https://registry.hub.docker.com/v2/repositories/place1/wg-access-server/tags?page_size=10') \ .read() \ .decode('utf-8') tags = json.loads(r).get('results', []) -print('current docker tags:', sorted( - [t.get('name') for t in tags], reverse=True)) +print('current docker tags:', sorted([t.get('name') for t in tags], reverse=True)) # tag the new image version = input('Version: ') docker_tag = f"place1/wg-access-server:{version}" -# subprocess.run(['docker', 'build', '-t', docker_tag, '.']) +subprocess.run(['docker', 'build', '-t', docker_tag, '.']) # update the helm chart and quickstart manifest -with open('deploy/helm/wg-access-server/Chart.yaml', 'r+') as f: - chart = yaml.load(f) - chart['version'] = version - chart['appVersion'] = version - f.seek(0) - yaml.dump(chart, f, default_flow_style=False) - f.truncate() -with open('deploy/k8s/quickstart.yaml', 'w') as f: - subprocess.run(['helm', 'template', '--name-template', - 'quickstart', 'deploy/helm/wg-access-server/'], stdout=f) -subprocess.run(['helm', 'package', 'deploy/helm/wg-access-server/', - '--destination', 'docs/charts/']) -subprocess.run(['helm', 'repo', 'index', 'docs/', '--url', - 'https://place1.github.io/wg-access-server']) +if not is_release_candidate(version): + with open('deploy/helm/wg-access-server/Chart.yaml', 'r+') as f: + chart = yaml.load(f) + chart['version'] = version + chart['appVersion'] = version + f.seek(0) + yaml.dump(chart, f, default_flow_style=False) + f.truncate() + with open('deploy/k8s/quickstart.yaml', 'w') as f: + subprocess.run(['helm', 'template', '--name-template', + 'quickstart', 'deploy/helm/wg-access-server/'], stdout=f) + subprocess.run(['helm', 'package', 'deploy/helm/wg-access-server/', + '--destination', 'docs/charts/']) + subprocess.run(['helm', 'repo', 'index', 'docs/', '--url', + 'https://place1.github.io/wg-access-server']) # commit changes -subprocess.run(['git', 'add', 'deploy']) +if not is_release_candidate(version): + subprocess.run(['git', 'add', 'deploy']) # tag the current commit -subprocess.run(['git', 'tag', '-a', f'v{version}', '-m', f'v{version}']) +if not is_release_candidate(version): + subprocess.run(['git', 'tag', '-a', f'v{version}', '-m', f'v{version}']) # push everything subprocess.run(['git', 'push']) diff --git a/requirements-docs.txt b/requirements-docs.txt new file mode 100644 index 0000000..67428a4 --- /dev/null +++ b/requirements-docs.txt @@ -0,0 +1,5 @@ +mkdocs-material==4.6.3 +mkdocs==1.1 +pymdown-extensions==6.3 +pygments~=2.5.2 +markdown-include==0.5.1 diff --git a/screenshots/devices.png b/screenshots/devices.png index 33e7d16..6bc290e 100644 Binary files a/screenshots/devices.png and b/screenshots/devices.png differ diff --git a/website/package-lock.json b/website/package-lock.json index a407c1a..c97a003 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -1364,11 +1364,6 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==" }, - "@nx-js/observer-util": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@nx-js/observer-util/-/observer-util-4.2.2.tgz", - "integrity": "sha512-9OayX1xkdGjdnsDiO2YdaYJ6aMyCF7/NY4QWVgIgjSAZJ4OX2fD766Ts79hEzBscenQy2DCaSoY8VkguIMB1ZA==" - }, "@svgr/babel-plugin-add-jsx-attribute": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-4.2.0.tgz", @@ -1547,6 +1542,12 @@ "integrity": "sha512-ifFemzjNchFBCtHS6bZNhSZCBu7tbtOe0e8qY0z2J4HtFXmPJjm6fXSaQsTG7yhShBEZtt2oP/bkwu5k+emlkQ==", "dev": true }, + "@types/history": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.5.tgz", + "integrity": "sha512-wLD/Aq2VggCJXSjxEwrMafIP51Z+13H78nXIX0ABEuIGhmB5sNGbR113MOKo+yfw+RDo1ZU3DM6yfnnRF/+ouw==", + "dev": true + }, "@types/istanbul-lib-coverage": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz", @@ -1592,6 +1593,12 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-13.5.0.tgz", "integrity": "sha512-Onhn+z72D2O2Pb2ql2xukJ55rglumsVo1H6Fmyi8mlU9SvKdBk/pUSUAiBY/d9bAOF7VVWajX3sths/+g6ZiAQ==" }, + "@types/numeral": { + "version": "0.0.26", + "resolved": "https://registry.npmjs.org/@types/numeral/-/numeral-0.0.26.tgz", + "integrity": "sha512-DwCsRqeOWopdEsm5KLTxKVKDSDoj+pzZD1vlwu1GQJ6IF3RhjuleYlRwyRH6MJLGaf3v8wFTnC6wo3yYfz0bnA==", + "dev": true + }, "@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -1633,6 +1640,27 @@ "@types/react": "*" } }, + "@types/react-router": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.4.tgz", + "integrity": "sha512-PZtnBuyfL07sqCJvGg3z+0+kt6fobc/xmle08jBiezLS8FrmGeiGkJnuxL/8Zgy9L83ypUhniV5atZn/L8n9MQ==", + "dev": true, + "requires": { + "@types/history": "*", + "@types/react": "*" + } + }, + "@types/react-router-dom": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.3.tgz", + "integrity": "sha512-pCq7AkOvjE65jkGS5fQwQhvUp4+4PVD9g39gXLZViP2UqFiFzsEpB3PKf0O6mdbKsewSK8N14/eegisa/0CwnA==", + "dev": true, + "requires": { + "@types/history": "*", + "@types/react": "*", + "@types/react-router": "*" + } + }, "@types/react-transition-group": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.2.3.tgz", @@ -4465,9 +4493,9 @@ } }, "date-fns": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.9.0.tgz", - "integrity": "sha512-khbFLu/MlzLjEzy9Gh8oY1hNt/Dvxw3J6Rbc28cVoYWQaC1S3YI4xwkF9ZWcjDLscbZlY9hISMr66RFzZagLsA==" + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.10.0.tgz", + "integrity": "sha512-EhfEKevYGWhWlZbNeplfhIU/+N+x0iCIx7VzKlXma2EdQyznVlZhCptXUY+BegNpPW2kjdx15Rvq503YcXXrcA==" }, "debug": { "version": "4.1.1", @@ -6412,6 +6440,11 @@ "integrity": "sha512-Fkbz1nyvvt6GC6ODcxh9Fen6LLB3OTCgGHzHwM2Eni44SUhzqPz1UQgFp9sfBEfInOhx3yBdwo9ZLjZAmJ+TtA==", "dev": true }, + "gud": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz", + "integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==" + }, "gzip-size": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz", @@ -6542,6 +6575,19 @@ "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==" }, + "history": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", + "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "requires": { + "@babel/runtime": "^7.1.2", + "loose-envify": "^1.2.0", + "resolve-pathname": "^3.0.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0", + "value-equal": "^1.0.1" + } + }, "hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -8999,6 +9045,16 @@ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" }, + "mini-create-react-context": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.3.2.tgz", + "integrity": "sha512-2v+OeetEyliMt5VHMXsBhABoJ0/M4RCe7fatd/fBy6SMiKazUSEt3gxxypfnk2SHMkdBYvorHRoQxuGoiwbzAw==", + "requires": { + "@babel/runtime": "^7.4.0", + "gud": "^1.0.0", + "tiny-warning": "^1.0.2" + } + }, "mini-css-extract-plugin": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.8.0.tgz", @@ -9144,6 +9200,29 @@ } } }, + "mobx": { + "version": "5.15.4", + "resolved": "https://registry.npmjs.org/mobx/-/mobx-5.15.4.tgz", + "integrity": "sha512-xRFJxSU2Im3nrGCdjSuOTFmxVDGeqOHL+TyADCGbT0k4HHqGmx5u2yaHNryvoORpI4DfbzjJ5jPmuv+d7sioFw==" + }, + "mobx-react": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/mobx-react/-/mobx-react-6.1.8.tgz", + "integrity": "sha512-NCMJn/hrWoeyeNbzCsBDtftWSy6VlFgw1VzhogrciPFvJIl2xs+8rJJdPlRHQTiNirwNoHNKJgUE4WhPZPvKDw==", + "requires": { + "mobx-react-lite": "^1.4.2" + } + }, + "mobx-react-lite": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/mobx-react-lite/-/mobx-react-lite-1.5.2.tgz", + "integrity": "sha512-PyZmARqqWtpuQaAoHF5pKX7h6TKNLwq6vtovm4zZvG6sEbMRHHSqioGXSeQbpRmG8Kw8uln3q/W1yMO5IfL5Sg==" + }, + "mobx-utils": { + "version": "5.5.5", + "resolved": "https://registry.npmjs.org/mobx-utils/-/mobx-utils-5.5.5.tgz", + "integrity": "sha512-N8832YcFRy3Hnl1L0YIbEKKAbwT2vXhGL3tzJsS8uI9Sp+R3osngUVFO2cbyOdApnWPBAe8KCFP0cbQuZzfXsA==" + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -9451,6 +9530,11 @@ "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, + "numeral": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/numeral/-/numeral-2.0.6.tgz", + "integrity": "sha1-StCAk21EPCVhrtnyGX7//iX05QY=" + }, "nwsapi": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", @@ -11432,14 +11516,6 @@ "scheduler": "^0.18.0" } }, - "react-easy-state": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/react-easy-state/-/react-easy-state-6.1.3.tgz", - "integrity": "sha512-uWQ7ittvJylwn/Xgz7Ub1jjsbpthQ9Ad1KDLxXfbXCb2OPnov4porVdnOJU2PKeRezcam3ZgfPUtf9L9rjtyWg==", - "requires": { - "@nx-js/observer-util": "^4.2.2" - } - }, "react-error-overlay": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.4.tgz", @@ -11450,6 +11526,52 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz", "integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==" }, + "react-router": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.1.2.tgz", + "integrity": "sha512-yjEuMFy1ONK246B+rsa0cUam5OeAQ8pyclRDgpxuSCrAlJ1qN9uZ5IgyKC7gQg0w8OM50NXHEegPh/ks9YuR2A==", + "requires": { + "@babel/runtime": "^7.1.2", + "history": "^4.9.0", + "hoist-non-react-statics": "^3.1.0", + "loose-envify": "^1.3.1", + "mini-create-react-context": "^0.3.0", + "path-to-regexp": "^1.7.0", + "prop-types": "^15.6.2", + "react-is": "^16.6.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "requires": { + "isarray": "0.0.1" + } + } + } + }, + "react-router-dom": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.1.2.tgz", + "integrity": "sha512-7BPHAaIwWpZS074UKaw1FjVdZBSVWEk8IuDXdB+OkLb8vd/WRQIpA4ag9WQk61aEfQs47wHyjWUoUGGZxpQXew==", + "requires": { + "@babel/runtime": "^7.1.2", + "history": "^4.9.0", + "loose-envify": "^1.3.1", + "prop-types": "^15.6.2", + "react-router": "5.1.2", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + } + }, "react-scripts": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-3.3.0.tgz", @@ -11861,6 +11983,11 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=" }, + "resolve-pathname": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", + "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==" + }, "resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", @@ -13171,6 +13298,11 @@ "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=" }, + "tiny-invariant": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz", + "integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==" + }, "tiny-warning": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", @@ -13596,6 +13728,11 @@ "spdx-expression-parse": "^3.0.0" } }, + "value-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", + "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==" + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/website/package.json b/website/package.json index fbfccd0..327bb6a 100644 --- a/website/package.json +++ b/website/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "codegen": "grpc-ts-web -o src/sdk ../proto/*.proto", + "codegen": "node_modules/.bin/grpc-ts-web -o src/sdk ../proto/*.proto", "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", @@ -18,11 +18,15 @@ "@types/react": "16.9.19", "@types/react-dom": "16.9.5", "common-tags": "^1.8.0", - "date-fns": "^2.9.0", + "date-fns": "^2.10.0", + "mobx": "^5.15.4", + "mobx-react": "^6.1.8", + "mobx-utils": "^5.5.5", + "numeral": "^2.0.6", "qrcode": "^1.4.4", "react": "^16.12.0", "react-dom": "^16.12.0", - "react-easy-state": "^6.1.3", + "react-router-dom": "^5.1.2", "react-scripts": "3.3.0", "tweetnacl-ts": "^1.0.3", "typeface-roboto": "0.0.75", @@ -46,7 +50,9 @@ "proxy": "http://localhost:8000", "devDependencies": { "@types/common-tags": "^1.8.0", + "@types/numeral": "0.0.26", "@types/qrcode": "^1.3.4", + "@types/react-router-dom": "^5.1.3", "grpc-ts-web": "0.1.6", "prettier": "^1.19.1" } diff --git a/website/src/Api.ts b/website/src/Api.ts index 16584e8..3d6a644 100644 --- a/website/src/Api.ts +++ b/website/src/Api.ts @@ -1,3 +1,4 @@ +import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb'; import { Devices } from './sdk/devices_pb'; import { Server } from './sdk/server_pb'; @@ -8,9 +9,23 @@ export const grpc = { devices: new Devices(backend), } - // https://github.com/SafetyCulture/grpc-web-devtools const devtools = (window as any).__GRPCWEB_DEVTOOLS__; if (devtools) { devtools(Object.values(grpc)); } + +// utils +export function toDate(timestamp: Timestamp.AsObject): Date { + const t = new Timestamp(); + t.setSeconds(timestamp.seconds); + t.setNanos(timestamp.nanos); + return t.toDate(); +} + +export function dateToTimestamp(date: Date): Timestamp.AsObject { + return { + seconds: Math.round(date.getTime() / 1000), + nanos: 0, + }; +} diff --git a/website/src/AppState.ts b/website/src/AppState.ts new file mode 100644 index 0000000..d4a4f7a --- /dev/null +++ b/website/src/AppState.ts @@ -0,0 +1,19 @@ +import { observable } from 'mobx'; +import { InfoRes } from './sdk/server_pb'; + +class GlobalAppState { + + @observable + info?: InfoRes.AsObject; + +} + +export const AppState = new GlobalAppState(); + +console.info('see global app state by typing "window.AppState"'); + +Object.assign(window as any, { + get AppState() { + return JSON.parse(JSON.stringify(AppState)); + } +}); diff --git a/website/src/Store.ts b/website/src/Store.ts deleted file mode 100644 index 54118fa..0000000 --- a/website/src/Store.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { store } from 'react-easy-state'; -import { Device } from './sdk/devices_pb'; - -export const AppState = store({ - devices: new Array(), -}); diff --git a/website/src/Util.ts b/website/src/Util.ts new file mode 100644 index 0000000..7e546fb --- /dev/null +++ b/website/src/Util.ts @@ -0,0 +1,46 @@ +import formatDistance from "date-fns/formatDistance"; +import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb'; +import { toDate } from "./Api"; +import { fromResource } from "mobx-utils"; + +export function sleep(seconds: number) { + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, seconds * 1000); + }); +} + +export function lastSeen(timestamp: Timestamp.AsObject | undefined): string { + if (timestamp === undefined) { + return 'Never'; + } + return formatDistance(toDate(timestamp), new Date(), { + addSuffix: true, + }); +} + +export function updatingDatasource(seconds: number, cb: () => Promise) { + let running = false; + let sink: ((next: T) => void) | undefined; + return { + update: async () => { + if (sink) { + sink(await cb()); + } + }, + ...fromResource( + async s => { + sink = s; + running = true; + while (running) { + sink(await cb()); + await sleep(seconds); + } + }, + () => { + running = false; + } + ) + } +} diff --git a/website/src/components/AddDevice.tsx b/website/src/components/AddDevice.tsx index f688966..90917fb 100644 --- a/website/src/components/AddDevice.tsx +++ b/website/src/components/AddDevice.tsx @@ -6,52 +6,47 @@ import DialogContent from '@material-ui/core/DialogContent'; import DialogTitle from '@material-ui/core/DialogTitle'; import FormControl from '@material-ui/core/FormControl'; import FormHelperText from '@material-ui/core/FormHelperText'; -import Grid from '@material-ui/core/Grid'; import Input from '@material-ui/core/Input'; import InputLabel from '@material-ui/core/InputLabel'; -import Paper from '@material-ui/core/Paper'; import AddIcon from '@material-ui/icons/Add'; import Typography from '@material-ui/core/Typography'; +import Card from '@material-ui/core/Card'; +import CardHeader from '@material-ui/core/CardHeader'; +import CardContent from '@material-ui/core/CardContent'; import qrcode from 'qrcode'; -import { makeStyles } from '@material-ui/core/styles'; import { codeBlock } from 'common-tags'; import { box_keyPair } from 'tweetnacl-ts'; -import { AppState } from '../Store'; +import { AppState } from '../AppState'; import { GetConnected } from './GetConnected'; import { grpc } from '../Api'; +import { observable } from 'mobx'; +import { observer } from 'mobx-react'; +interface Props { + onAdd: () => void; +} + +@observer +export class AddDevice extends React.Component { + + @observable + dialogOpen = false; + + @observable + error?: string; -const useStyles = makeStyles(theme => ({ - hidden: { - display: 'none', - }, - button: { - margin: theme.spacing(1), - }, - fabButton: { - position: 'absolute', - margin: '0 auto', - left: 0, - right: 0, - }, - paper: { - padding: theme.spacing(2), - }, -})); - -export default function AddDevice() { - const classes = useStyles(); - const [dialogOpen, setDialogOpen] = React.useState(false); - const [error, setError] = React.useState(''); - const [name, setName] = React.useState(''); - const [qrCodeUri, setQrCodeUri] = React.useState(''); - const [configFileUri, setConfigFileUri] = React.useState(''); - - const reset = () => { - setName(''); + @observable + formState = { + name: '', }; - const addDevice = async (event: React.FormEvent) => { + @observable + qrCode?: string; + + @observable + configFileUri?: string; + + submit = async (event: React.FormEvent) => { event.preventDefault(); const keypair = box_keyPair(); @@ -59,9 +54,13 @@ export default function AddDevice() { const privateKey = window.btoa(String.fromCharCode(...(new Uint8Array(keypair.secretKey) as any))); try { - const device = await grpc.devices.addDevice({ name, publicKey }); - const info = await grpc.server.info({}); - AppState.devices.push(device); + const device = await grpc.devices.addDevice({ + name: this.formState.name, + publicKey, + }); + this.props.onAdd(); + + const info = AppState.info!; const configFile = codeBlock` [Interface] PrivateKey = ${privateKey} @@ -73,40 +72,47 @@ export default function AddDevice() { AllowedIPs = 0.0.0.0/1, 128.0.0.0/1, ::/0 Endpoint = ${`${info.host?.value || window.location.hostname}:${info.port || '51820'}`} `; - setQrCodeUri(await qrcode.toDataURL(configFile)); - setConfigFileUri(URL.createObjectURL(new Blob([configFile]))); - reset(); - setDialogOpen(true); + + this.qrCode = await qrcode.toDataURL(configFile); + this.configFileUri = URL.createObjectURL(new Blob([configFile])); + this.dialogOpen = true; + this.reset(); + } catch (error) { console.log(error); - setError('failed'); + // TODO: unwrap grpc error message + this.error = 'failed'; } - }; + } - return ( - - - - - -

Add A Device

-
- + reset = () => { + this.formState.name = ''; + } + + render() { + return ( + <> + + + + + Device Name setName(event.currentTarget.value)} + value={this.formState.name} + onChange={(event) => this.formState.name = event.currentTarget.value} aria-describedby="device-name-text" /> - {error} + {this.error} @@ -115,35 +121,33 @@ export default function AddDevice() { variant="contained" endIcon={} type="submit" - className={classes.button} > Add -
-
- -
- - Get Connected - - - - - - - -
- ); + + + + Get Connected + + + + + + + + + ); + } } diff --git a/website/src/components/DeviceListItem.tsx b/website/src/components/DeviceListItem.tsx index cc6b153..7e40e05 100644 --- a/website/src/components/DeviceListItem.tsx +++ b/website/src/components/DeviceListItem.tsx @@ -2,36 +2,32 @@ import React from 'react'; import Card from '@material-ui/core/Card'; import CardHeader from '@material-ui/core/CardHeader'; import CardContent from '@material-ui/core/CardContent'; -import Typography from '@material-ui/core/Typography'; import Avatar from '@material-ui/core/Avatar'; -import DonutSmallIcon from '@material-ui/icons/DonutSmall'; +import WifiIcon from '@material-ui/icons/Wifi'; +import WifiOffIcon from '@material-ui/icons/WifiOff'; import MenuItem from '@material-ui/core/MenuItem'; -import formatDistanceToNow from 'date-fns/formatDistanceToNow'; -import { view } from 'react-easy-state'; -import { AppState } from '../Store'; +import numeral from 'numeral'; +import { lastSeen } from '../Util'; +import { AppState } from '../AppState'; import { IconMenu } from './IconMenu'; import { PopoverDisplay } from './PopoverDisplay'; import { Device } from '../sdk/devices_pb' import { grpc } from '../Api'; +import { observer } from 'mobx-react'; interface Props { device: Device.AsObject; + onRemove: () => void; } -class DeviceListItem extends React.Component { - dateString(date: Date) { - if (date.getUTCMilliseconds() === 0) { - return 'never'; - } - return formatDistanceToNow(date, { addSuffix: true }); - } - +@observer +export class DeviceListItem extends React.Component { removeDevice = async () => { try { await grpc.devices.deleteDevice({ name: this.props.device.name, }); - AppState.devices = AppState.devices.filter(device => device.name !== this.props.device.name); + this.props.onRemove(); } catch { window.alert('api request failed'); } @@ -44,8 +40,12 @@ class DeviceListItem extends React.Component { - + + {/* */} + {device.connected + ? + : + } } action={ @@ -57,13 +57,43 @@ class DeviceListItem extends React.Component { } /> - - Public Key: {device.publicKey} - + + + {AppState.info?.metadataEnabled && device.connected && + <> + + + + + + + + + + + + + + } + {AppState.info?.metadataEnabled && !device.connected && + <> + + + + + + + + + } + + + + + +
Endpoint{device.endpoint}
Sent{numeral(device.transmitBytes).format('0b')}
Received{numeral(device.receiveBytes).format('0b')}
Disconnected
Last Seen{lastSeen(device.lastHandshakeTime)}
Public key{device.publicKey}
); } } - -export default view(DeviceListItem); diff --git a/website/src/components/Devices.tsx b/website/src/components/Devices.tsx index d6e8ef8..b108c30 100644 --- a/website/src/components/Devices.tsx +++ b/website/src/components/Devices.tsx @@ -1,32 +1,39 @@ import React from 'react'; import Grid from '@material-ui/core/Grid'; -import { view } from 'react-easy-state'; -import { AppState } from '../Store'; -import DeviceListItem from './DeviceListItem'; +import { observable } from 'mobx'; +import { observer } from 'mobx-react'; import { grpc } from '../Api'; +import { updatingDatasource as autorefresh } from '../Util'; +import { DeviceListItem } from './DeviceListItem'; +import { AddDevice } from './AddDevice'; -class Devices extends React.Component { +@observer +export class Devices extends React.Component { - componentDidMount() { - this.load(); - } - - async load() { - const res = await grpc.devices.listDevices({}); - AppState.devices = res.items; - } + @observable + devices = autorefresh(5, async () => { + return (await grpc.devices.listDevices({})).items; + }); render() { + if (!this.devices.current()) { + return

loading...

+ } return ( - - {AppState.devices.map((device, i) => ( - - + + + + {this.devices.current().map((device, i) => ( + + this.devices.update()} /> + + ))} - ))} + + + this.devices.update()} /> + ); } } - -export default view(Devices); diff --git a/website/src/components/Navigation.tsx b/website/src/components/Navigation.tsx index a671138..b584fc0 100644 --- a/website/src/components/Navigation.tsx +++ b/website/src/components/Navigation.tsx @@ -1,11 +1,14 @@ import React from 'react'; import { makeStyles } from '@material-ui/core/styles'; +import { getCookie } from '../Cookies'; +import { AppState } from '../AppState'; +import { NavLink } from 'react-router-dom'; import AppBar from '@material-ui/core/AppBar'; import Toolbar from '@material-ui/core/Toolbar'; import Typography from '@material-ui/core/Typography'; import Link from '@material-ui/core/Link'; import Button from '@material-ui/core/Button'; -import { getCookie } from '../Cookies'; +import Chip from '@material-ui/core/Chip'; const useStyles = makeStyles(theme => ({ title: { @@ -20,10 +23,22 @@ export default function Navigation() { - Your Devices + Welcome + {AppState.info?.isAdmin && + + } + + {AppState.info?.isAdmin && + + + + } + {hasAuthCookie && - + diff --git a/website/src/components/PopoverDisplay.tsx b/website/src/components/PopoverDisplay.tsx index ce1f950..6bb0e24 100644 --- a/website/src/components/PopoverDisplay.tsx +++ b/website/src/components/PopoverDisplay.tsx @@ -15,7 +15,7 @@ export class PopoverDisplay extends React.Component { render() { return ( - { - return ( - - - - - - - - - ); -}); + render() { + if (!AppState.info) { + return

loading...

+ } + return ( + + + + + + + {AppState.info.isAdmin && + <> + + + } + + + + ); + } +} ReactDOM.render(, document.getElementById('root')); diff --git a/website/src/pages/YourDevices.tsx b/website/src/pages/YourDevices.tsx new file mode 100644 index 0000000..e78085d --- /dev/null +++ b/website/src/pages/YourDevices.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { Devices } from '../components/Devices'; + +export class YourDevices extends React.Component { + render() { + return ( + + ); + } +} diff --git a/website/src/pages/admin/AllDevices.tsx b/website/src/pages/admin/AllDevices.tsx new file mode 100644 index 0000000..b46fa81 --- /dev/null +++ b/website/src/pages/admin/AllDevices.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import Table from '@material-ui/core/Table'; +import TableBody from '@material-ui/core/TableBody'; +import TableCell from '@material-ui/core/TableCell'; +import TableContainer from '@material-ui/core/TableContainer'; +import TableHead from '@material-ui/core/TableHead'; +import TableRow from '@material-ui/core/TableRow'; +import { observer } from 'mobx-react'; +import { lazyObservable } from 'mobx-utils'; +import { grpc } from '../../Api'; +import { Device } from '../../sdk/devices_pb'; +import { lastSeen } from '../../Util'; + +@observer +export class AllDevices extends React.Component { + + devices = lazyObservable(async sink => { + const res = await grpc.devices.listAllDevices({}); + sink(res.items); + }); + + render() { + if (!this.devices.current()) { + return

loading...

+ } + + const rows = this.devices.current(); + + return ( + + + + + Owner + Device + Connected + Last Seen + + + + {rows.map((row, i) => ( + + + {row.owner} + + {row.name} + {row.connected ? 'yes' : 'no'} + {lastSeen(row.lastHandshakeTime)} + + ))} + +
+
+ ); + } +} diff --git a/website/src/sdk/devices_pb.ts b/website/src/sdk/devices_pb.ts index 71524cc..189c1a5 100644 --- a/website/src/sdk/devices_pb.ts +++ b/website/src/sdk/devices_pb.ts @@ -32,6 +32,12 @@ export class Devices { googleProtobufEmpty.Empty.deserializeBinary ); + private methodInfoListAllDevices = new grpcWeb.AbstractClientBase.MethodInfo( + ListAllDevicesRes, + (req: ListAllDevicesReq) => req.serializeBinary(), + ListAllDevicesRes.deserializeBinary + ); + constructor( private hostname: string, private defaultMetadata?: () => grpcWeb.Metadata, @@ -94,6 +100,25 @@ export class Devices { }); } + listAllDevices(req: ListAllDevicesReq.AsObject, metadata?: grpcWeb.Metadata): Promise { + return new Promise((resolve, reject) => { + const message = ListAllDevicesReqFromObject(req); + this.client_.rpcCall( + this.hostname + '/proto.Devices/ListAllDevices', + message, + Object.assign({}, this.defaultMetadata ? this.defaultMetadata() : {}, metadata), + this.methodInfoListAllDevices, + (err: grpcWeb.Error, res: ListAllDevicesRes) => { + if (err) { + reject(err); + } else { + resolve(res.toObject()); + } + }, + ); + }); + } + } @@ -106,6 +131,11 @@ export declare namespace Device { publicKey: string, address: string, createdAt?: googleProtobufTimestamp.Timestamp.AsObject, + connected: boolean, + lastHandshakeTime?: googleProtobufTimestamp.Timestamp.AsObject, + receiveBytes: number, + transmitBytes: number, + endpoint: string, } } @@ -161,6 +191,46 @@ export class Device extends jspb.Message { (jspb.Message as any).setWrapperField(this, 5, value); } + getConnected(): boolean { + return jspb.Message.getFieldWithDefault(this, 6, false); + } + + setConnected(value: boolean): void { + (jspb.Message as any).setProto3BooleanField(this, 6, value); + } + + getLastHandshakeTime(): googleProtobufTimestamp.Timestamp { + return jspb.Message.getWrapperField(this, googleProtobufTimestamp.Timestamp, 7); + } + + setLastHandshakeTime(value?: googleProtobufTimestamp.Timestamp): void { + (jspb.Message as any).setWrapperField(this, 7, value); + } + + getReceiveBytes(): number { + return jspb.Message.getFieldWithDefault(this, 8, 0); + } + + setReceiveBytes(value: number): void { + (jspb.Message as any).setProto3IntField(this, 8, value); + } + + getTransmitBytes(): number { + return jspb.Message.getFieldWithDefault(this, 9, 0); + } + + setTransmitBytes(value: number): void { + (jspb.Message as any).setProto3IntField(this, 9, value); + } + + getEndpoint(): string { + return jspb.Message.getFieldWithDefault(this, 10, ""); + } + + setEndpoint(value: string): void { + (jspb.Message as any).setProto3StringField(this, 10, value); + } + serializeBinary(): Uint8Array { const writer = new jspb.BinaryWriter(); Device.serializeBinaryToWriter(this, writer); @@ -174,6 +244,11 @@ export class Device extends jspb.Message { publicKey: this.getPublicKey(), address: this.getAddress(), createdAt: (f = this.getCreatedAt()) && f.toObject(), + connected: this.getConnected(), + lastHandshakeTime: (f = this.getLastHandshakeTime()) && f.toObject(), + receiveBytes: this.getReceiveBytes(), + transmitBytes: this.getTransmitBytes(), + endpoint: this.getEndpoint(), }; } @@ -199,6 +274,26 @@ export class Device extends jspb.Message { if (field5 != null) { writer.writeMessage(5, field5, googleProtobufTimestamp.Timestamp.serializeBinaryToWriter); } + const field6 = message.getConnected(); + if (field6 != false) { + writer.writeBool(6, field6); + } + const field7 = message.getLastHandshakeTime(); + if (field7 != null) { + writer.writeMessage(7, field7, googleProtobufTimestamp.Timestamp.serializeBinaryToWriter); + } + const field8 = message.getReceiveBytes(); + if (field8 != 0) { + writer.writeInt64(8, field8); + } + const field9 = message.getTransmitBytes(); + if (field9 != 0) { + writer.writeInt64(9, field9); + } + const field10 = message.getEndpoint(); + if (field10.length > 0) { + writer.writeString(10, field10); + } } static deserializeBinary(bytes: Uint8Array): Device { @@ -235,6 +330,27 @@ export class Device extends jspb.Message { reader.readMessage(field5, googleProtobufTimestamp.Timestamp.deserializeBinaryFromReader); message.setCreatedAt(field5); break; + case 6: + const field6 = reader.readBool() + message.setConnected(field6); + break; + case 7: + const field7 = new googleProtobufTimestamp.Timestamp(); + reader.readMessage(field7, googleProtobufTimestamp.Timestamp.deserializeBinaryFromReader); + message.setLastHandshakeTime(field7); + break; + case 8: + const field8 = reader.readInt64() + message.setReceiveBytes(field8); + break; + case 9: + const field9 = reader.readInt64() + message.setTransmitBytes(field9); + break; + case 10: + const field10 = reader.readString() + message.setEndpoint(field10); + break; default: reader.skipField(); break; @@ -537,6 +653,137 @@ export class DeleteDeviceReq extends jspb.Message { } } +export declare namespace ListAllDevicesReq { + export type AsObject = { + } +} + +export class ListAllDevicesReq extends jspb.Message { + + private static repeatedFields_ = [ + + ]; + + constructor(data?: jspb.Message.MessageArray) { + super(); + jspb.Message.initialize(this, data || [], 0, -1, ListAllDevicesReq.repeatedFields_, null); + } + + + serializeBinary(): Uint8Array { + const writer = new jspb.BinaryWriter(); + ListAllDevicesReq.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); + } + + toObject(): ListAllDevicesReq.AsObject { + let f: any; + return { + }; + } + + static serializeBinaryToWriter(message: ListAllDevicesReq, writer: jspb.BinaryWriter): void { + } + + static deserializeBinary(bytes: Uint8Array): ListAllDevicesReq { + var reader = new jspb.BinaryReader(bytes); + var message = new ListAllDevicesReq(); + return ListAllDevicesReq.deserializeBinaryFromReader(message, reader); + } + + static deserializeBinaryFromReader(message: ListAllDevicesReq, reader: jspb.BinaryReader): ListAllDevicesReq { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + const field = reader.getFieldNumber(); + switch (field) { + default: + reader.skipField(); + break; + } + } + return message; + } + +} +export declare namespace ListAllDevicesRes { + export type AsObject = { + items: Array, + } +} + +export class ListAllDevicesRes extends jspb.Message { + + private static repeatedFields_ = [ + 1, + ]; + + constructor(data?: jspb.Message.MessageArray) { + super(); + jspb.Message.initialize(this, data || [], 0, -1, ListAllDevicesRes.repeatedFields_, null); + } + + + getItems(): Array { + return jspb.Message.getRepeatedWrapperField(this, Device, 1); + } + + setItems(value: Array): void { + (jspb.Message as any).setRepeatedWrapperField(this, 1, value); + } + + addItems(value?: Device, index?: number): Device { + return jspb.Message.addToRepeatedWrapperField(this, 1, value, Device, index); + } + + serializeBinary(): Uint8Array { + const writer = new jspb.BinaryWriter(); + ListAllDevicesRes.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); + } + + toObject(): ListAllDevicesRes.AsObject { + let f: any; + return { + items: this.getItems().map((item) => item.toObject()), + }; + } + + static serializeBinaryToWriter(message: ListAllDevicesRes, writer: jspb.BinaryWriter): void { + const field1 = message.getItems(); + if (field1.length > 0) { + writer.writeRepeatedMessage(1, field1, Device.serializeBinaryToWriter); + } + } + + static deserializeBinary(bytes: Uint8Array): ListAllDevicesRes { + var reader = new jspb.BinaryReader(bytes); + var message = new ListAllDevicesRes(); + return ListAllDevicesRes.deserializeBinaryFromReader(message, reader); + } + + static deserializeBinaryFromReader(message: ListAllDevicesRes, reader: jspb.BinaryReader): ListAllDevicesRes { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + const field = reader.getFieldNumber(); + switch (field) { + case 1: + const field1 = new Device(); + reader.readMessage(field1, Device.deserializeBinaryFromReader); + message.addItems(field1); + break; + default: + reader.skipField(); + break; + } + } + return message; + } + +} function DeviceFromObject(obj: Device.AsObject | undefined): Device | undefined { @@ -549,6 +796,11 @@ function DeviceFromObject(obj: Device.AsObject | undefined): Device | undefined message.setPublicKey(obj.publicKey); message.setAddress(obj.address); message.setCreatedAt(TimestampFromObject(obj.createdAt)); + message.setConnected(obj.connected); + message.setLastHandshakeTime(TimestampFromObject(obj.lastHandshakeTime)); + message.setReceiveBytes(obj.receiveBytes); + message.setTransmitBytes(obj.transmitBytes); + message.setEndpoint(obj.endpoint); return message; } @@ -600,6 +852,25 @@ function DeleteDeviceReqFromObject(obj: DeleteDeviceReq.AsObject | undefined): D return message; } +function ListAllDevicesReqFromObject(obj: ListAllDevicesReq.AsObject | undefined): ListAllDevicesReq | undefined { + if (obj === undefined) { + return undefined; + } + const message = new ListAllDevicesReq(); + return message; +} + +function ListAllDevicesResFromObject(obj: ListAllDevicesRes.AsObject | undefined): ListAllDevicesRes | undefined { + if (obj === undefined) { + return undefined; + } + const message = new ListAllDevicesRes(); + (obj.items || []) + .map((item) => DeviceFromObject(item)) + .forEach((item) => message.addItems(item)); + return message; +} + function EmptyFromObject(obj: googleProtobufEmpty.Empty.AsObject | undefined): googleProtobufEmpty.Empty | undefined { if (obj === undefined) { return undefined; diff --git a/website/src/sdk/server_pb.ts b/website/src/sdk/server_pb.ts index 321f835..601ad8b 100644 --- a/website/src/sdk/server_pb.ts +++ b/website/src/sdk/server_pb.ts @@ -108,6 +108,8 @@ export declare namespace InfoRes { host?: googleProtobufWrappers.StringValue.AsObject, port: number, hostVpnIp: string, + metadataEnabled: boolean, + isAdmin: boolean, } } @@ -155,6 +157,22 @@ export class InfoRes extends jspb.Message { (jspb.Message as any).setProto3StringField(this, 4, value); } + getMetadataEnabled(): boolean { + return jspb.Message.getFieldWithDefault(this, 5, false); + } + + setMetadataEnabled(value: boolean): void { + (jspb.Message as any).setProto3BooleanField(this, 5, value); + } + + getIsAdmin(): boolean { + return jspb.Message.getFieldWithDefault(this, 6, false); + } + + setIsAdmin(value: boolean): void { + (jspb.Message as any).setProto3BooleanField(this, 6, value); + } + serializeBinary(): Uint8Array { const writer = new jspb.BinaryWriter(); InfoRes.serializeBinaryToWriter(this, writer); @@ -167,6 +185,8 @@ export class InfoRes extends jspb.Message { host: (f = this.getHost()) && f.toObject(), port: this.getPort(), hostVpnIp: this.getHostVpnIp(), + metadataEnabled: this.getMetadataEnabled(), + isAdmin: this.getIsAdmin(), }; } @@ -188,6 +208,14 @@ export class InfoRes extends jspb.Message { if (field4.length > 0) { writer.writeString(4, field4); } + const field5 = message.getMetadataEnabled(); + if (field5 != false) { + writer.writeBool(5, field5); + } + const field6 = message.getIsAdmin(); + if (field6 != false) { + writer.writeBool(6, field6); + } } static deserializeBinary(bytes: Uint8Array): InfoRes { @@ -220,6 +248,14 @@ export class InfoRes extends jspb.Message { const field4 = reader.readString() message.setHostVpnIp(field4); break; + case 5: + const field5 = reader.readBool() + message.setMetadataEnabled(field5); + break; + case 6: + const field6 = reader.readBool() + message.setIsAdmin(field6); + break; default: reader.skipField(); break; @@ -248,6 +284,8 @@ function InfoResFromObject(obj: InfoRes.AsObject | undefined): InfoRes | undefin message.setHost(StringValueFromObject(obj.host)); message.setPort(obj.port); message.setHostVpnIp(obj.hostVpnIp); + message.setMetadataEnabled(obj.metadataEnabled); + message.setIsAdmin(obj.isAdmin); return message; } diff --git a/website/tsconfig.json b/website/tsconfig.json index af10394..2d971e4 100644 --- a/website/tsconfig.json +++ b/website/tsconfig.json @@ -13,7 +13,8 @@ "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, - "jsx": "react" + "jsx": "react", + "experimentalDecorators": true }, "include": ["src"] }