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
|
||||
|
||||
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"`
|
@ -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 ReactDOM from 'react-dom';
|
||||
import CssBaseline from '@material-ui/core/CssBaseline';
|
||||
import Box from '@material-ui/core/Box';
|
||||
import AddDevice from './components/AddDevice';
|
||||
import Devices from './components/Devices';
|
||||
import Navigation from './components/Navigation';
|
||||
import { view } from 'react-easy-state';
|
||||
import 'typeface-roboto';
|
||||
import {
|
||||
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(() => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<CssBaseline />
|
||||
<Navigation />
|
||||
<Box component="div" m={3}>
|
||||
<Devices />
|
||||
<AddDevice />
|
||||
</Box>
|
||||
</React.Fragment>
|
||||
);
|
||||
});
|
||||
render() {
|
||||
if (!AppState.info) {
|
||||
return <p>loading...</p>
|
||||
}
|
||||
return (
|
||||
<Router>
|
||||
<CssBaseline />
|
||||
<Navigation />
|
||||
<Box component="div" m={2}>
|
||||
<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'));
|
||||
|
@ -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