basic admin feature, network isolation, docs, helm, k8s, docs (#15)
* wip * wip * wip * wip * wip * wip * helm update * wip * wip * wip * secret for private key * updated publish script * wip * refactored to mobx, added list all devices for admins * dockerfile fix * fixed basic auth * healthcheck fix * removed healthcheck because it caused issues with traefik * helm chart updates * wip * wip * super basic healthcheck endpoint * wip * added changelog, updated docspull/17/head
parent
e34f21813a
commit
304a6526cc
@ -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 :)
|
@ -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
|
@ -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: "<loadbalancer-ip>:51820"
|
||||||
|
wireguard:
|
||||||
|
config:
|
||||||
|
privateKey: "<wireguard-private-key>"
|
||||||
|
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 | `[]` | |
|
@ -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 }}
|
@ -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="<value>"`.
|
||||||
|
|
||||||
|
## 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 <username>"
|
||||||
|
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
|
||||||
|
```
|
@ -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
|
||||||
|
```
|
@ -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!}
|
||||||
|
```
|
@ -0,0 +1,3 @@
|
|||||||
|
# Helm Chart
|
||||||
|
|
||||||
|
{!../deploy/helm/wg-access-server/README.md!}
|
@ -0,0 +1,3 @@
|
|||||||
|
# Welcome
|
||||||
|
|
||||||
|
{!../README.md!}
|
@ -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"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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"
|
||||||
|
}
|
@ -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
|
|
||||||
}
|
|
@ -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
|
@ -1,6 +1,6 @@
|
|||||||
package authconfig
|
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 {
|
type GitlabConfig struct {
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"`
|
@ -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
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package authsession
|
||||||
|
|
||||||
|
type Identity struct {
|
||||||
|
Subject string
|
||||||
|
Claims Claims
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
package authsession
|
||||||
|
|
||||||
|
type ClaimsMiddleware func(user *Identity) error
|
@ -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
|
Binary file not shown.
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 87 KiB |
@ -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));
|
||||||
|
}
|
||||||
|
});
|
@ -1,6 +0,0 @@
|
|||||||
import { store } from 'react-easy-state';
|
|
||||||
import { Device } from './sdk/devices_pb';
|
|
||||||
|
|
||||||
export const AppState = store({
|
|
||||||
devices: new Array<Device.AsObject>(),
|
|
||||||
});
|
|
@ -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<T>(seconds: number, cb: () => Promise<T>) {
|
||||||
|
let running = false;
|
||||||
|
let sink: ((next: T) => void) | undefined;
|
||||||
|
return {
|
||||||
|
update: async () => {
|
||||||
|
if (sink) {
|
||||||
|
sink(await cb());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...fromResource<T>(
|
||||||
|
async s => {
|
||||||
|
sink = s;
|
||||||
|
running = true;
|
||||||
|
while (running) {
|
||||||
|
sink(await cb());
|
||||||
|
await sleep(seconds);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
running = false;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -1,24 +1,48 @@
|
|||||||
|
import 'typeface-roboto';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import CssBaseline from '@material-ui/core/CssBaseline';
|
import CssBaseline from '@material-ui/core/CssBaseline';
|
||||||
import Box from '@material-ui/core/Box';
|
import Box from '@material-ui/core/Box';
|
||||||
import AddDevice from './components/AddDevice';
|
|
||||||
import Devices from './components/Devices';
|
|
||||||
import Navigation from './components/Navigation';
|
import Navigation from './components/Navigation';
|
||||||
import { view } from 'react-easy-state';
|
import {
|
||||||
import 'typeface-roboto';
|
BrowserRouter as Router,
|
||||||
|
Switch,
|
||||||
|
Route,
|
||||||
|
} from 'react-router-dom';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import { grpc } from './Api';
|
||||||
|
import { AppState } from './AppState';
|
||||||
|
import { YourDevices } from './pages/YourDevices';
|
||||||
|
import { AllDevices } from './pages/admin/AllDevices';
|
||||||
|
|
||||||
|
@observer
|
||||||
|
class App extends React.Component {
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
|
AppState.info = await grpc.server.info({});
|
||||||
|
}
|
||||||
|
|
||||||
const App = view(() => {
|
render() {
|
||||||
return (
|
if (!AppState.info) {
|
||||||
<React.Fragment>
|
return <p>loading...</p>
|
||||||
<CssBaseline />
|
}
|
||||||
<Navigation />
|
return (
|
||||||
<Box component="div" m={3}>
|
<Router>
|
||||||
<Devices />
|
<CssBaseline />
|
||||||
<AddDevice />
|
<Navigation />
|
||||||
</Box>
|
<Box component="div" m={2}>
|
||||||
</React.Fragment>
|
<Switch>
|
||||||
);
|
<Route exact path="/" component={YourDevices} />
|
||||||
});
|
{AppState.info.isAdmin &&
|
||||||
|
<>
|
||||||
|
<Route exact path="/admin/all-devices" component={AllDevices} />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</Switch>
|
||||||
|
</Box>
|
||||||
|
</Router>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ReactDOM.render(<App />, document.getElementById('root'));
|
ReactDOM.render(<App />, document.getElementById('root'));
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Devices } from '../components/Devices';
|
||||||
|
|
||||||
|
export class YourDevices extends React.Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Devices />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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<Device.AsObject[]>(async sink => {
|
||||||
|
const res = await grpc.devices.listAllDevices({});
|
||||||
|
sink(res.items);
|
||||||
|
});
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (!this.devices.current()) {
|
||||||
|
return <p>loading...</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = this.devices.current();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableContainer>
|
||||||
|
<Table stickyHeader>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Owner</TableCell>
|
||||||
|
<TableCell>Device</TableCell>
|
||||||
|
<TableCell>Connected</TableCell>
|
||||||
|
<TableCell>Last Seen</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{rows.map((row, i) => (
|
||||||
|
<TableRow key={i}>
|
||||||
|
<TableCell component="th" scope="row">
|
||||||
|
{row.owner}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{row.name}</TableCell>
|
||||||
|
<TableCell>{row.connected ? 'yes' : 'no'}</TableCell>
|
||||||
|
<TableCell>{lastSeen(row.lastHandshakeTime)}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue