big-refactor
James Batt 4 years ago
parent 7dd4d2b4b5
commit dff8fcefb1

1
.gitignore vendored

@ -1,2 +1,3 @@
config.yaml
data/
wg-access-server

@ -23,6 +23,7 @@ RUN go build -o server
FROM alpine:3.10
RUN apk add iptables
RUN apk add wireguard-tools
ENV CONFIG="/config.yaml"
ENV STORAGE_DIRECTORY="/data"
COPY --from=server /code/server /server
COPY --from=website /code/build /website/build

@ -1,6 +1,4 @@
# WG Access Server
_i'm still thinking of a name..._
# wg-access-server
## What is this
@ -8,120 +6,94 @@ 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 that's simple to use.
Today, this project allows you to deploy a WireGuard VPN using a single
docker container; use a web ui to add/connect your Linux/Mac/Windows/iOS/Android
device; and manage connected devices. The server will automatically
configure ip routes and iptables rules to ensure that client VPN traffic
can access the internet.
The docker container runs wireguard in userspace using [boringtun](https://github.com/cloudflare/boringtun)
and only required NET_ADMIN plus access to /dev/net/tun.
The privileges are required by boringtun to create a userspace tun/tap device
which is a userspace virtual network interface ([wikipedia](https://en.wikipedia.org/wiki/TUN/TAP))
and for the software to configure iptables and network routes within it's network
namespace. The container doesn't require host networking but it can be used so that
VPN client's can access IP addresses otherwise accessible from the host's network.
Soon I hope to add the following features
- [ ] headless mode
* in this mode there'll be no web ui
* you can add devices (i.e. WireGuard peers) via files, flags or the environment
* intended for use by developers to easily deploy a one-shot style
VPN into a network to get access to it on their local machine,
i'm hoping to use this mode to VPN into a kubernetes cluster's
overlay network including DNS and cluster service routing.
- [x] singleuser mode
* this is how the project currently works but I'll expand it to support authentication
- [x] multiuser mode
- [x] support pluggable authentication backends including OAuth, OpenID Connect, LDAP, etc.
- [x] allow different users to manage thier own devices without seeing others
- [ ] allow network isolation to be turned on or off allowing users to communicate or be isolated
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.
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)).
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.
## 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.15:8000
using my laptop's LAN IP address.
```
docker run \
-it \
--rm \
--cap-add NET_ADMIN \
--device /dev/net/tun:/dev/net/tun \
-v wg-access-server-data:/data \
-p 8000:8000/tcp \
-p 51820:51820/udp \
place1/wg-access-server
```
To use a custom [configuration](#configuration) file, please add a `CONFIG` environment variable and make sure the configuration file is mounted:
```
...
-e CONFIG=/config/config.yaml
-v ./config.yaml:/config/config.yaml
...
```
## Configuration
You can configure the server using a config file.
You can configure the server using a yaml configuration file. Just mount the file into the container like this:
```bash
sudo go run ./main.go --config ./config.yaml
```
docker run \
... \
-v $(pwd)/config.yaml:/config.yaml \
place1/wg-access-server
```
Here's an example showing the default values:
```yaml
loglevel: debug
web:
// ExternalAddress is that users access the web ui
// using. This value is required for using auth backends
// This value should include the scheme.
// The port should be included if non-standard.
// e.g. http://192.168.0.2:8000
// or https://myvpn.example.com
externalAddress: ""
// Port that the web server should listen on
port: 8000
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).
directory: ""
// Defaults to "/data" inside the docker container
directory: /data
wireguard:
// UserspaceImplementation is a command (program on $PATH)
// that implements the WireGuard protocol in userspace.
// In our Docker image we make use of `boringtun` so that
// users aren't required to setup kernel modules.
// You can leave this value empty if you want to run wireguard
// in-kernal. The server will still connect to the "WireGuard.InterfaceName"
userspaceImplementation: ""
// The network interface name for wireguard
// Optional
interfaceName: wg0
// The WireGuard PrivateKey
// If this value is lost then any existing
// clients (WireGuard peers) will no longer
// be able to connect.
// Clients will either have to manually update
// their connection configuration or setup
// their VPN again using the web ui (easier for most people)
// 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 users
// ExternalAddress is the address that clients
// use to connect to the wireguard interface
// This value is used in the generated client config
// files. If this value is empty then the frontend
// will use `${window.location.hostname}:51820`
externalAddress: ""
// 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
// 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
@ -129,63 +101,68 @@ vpn:
// to the outside internet
// If not configured then the server will select the default
// network interface e.g. eth0
// Optional
gatewayInterface: ""
dns:
// The upstream DNS servers that VPN clients will use
// VPN Clients will connect to a DNS proxy running on the
// wireguard server, which will send DNS requests to this
// upstream server.
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:
// The below are all optional.
// Different authentication backends can be configured.
// If no authentication backends are configured then
// the server will not require authentication.
// The server embeds dex to provide the authentication
// integrations - https://github.com/dexidp/dex
// 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: ""
gitlab:
name: ""
baseURL: ""
clientID: ""
clientSecret: ""
}
redirectURL: ""
```
You can also set some configuration via environment variables:
## Screenshots
```bash
export LOG_LEVEL="info"
export STORAGE_DIRECTORY="/my-data"
export WIREGUARD_PRIVATE_KEY="$(wg genkey)"
sudo go run ./main.go
```
![Connect iOS](./screenshots/connect-ios.png)
## Screenshots
![Connect MacOS](./screenshots/connect-macos.png)
![IOS Connection Dialog](./screenshots/get-connected-ios.png)
![Devices](./screenshots/devices.png)
![Windows Connection Dialog](./screenshots/get-connected-windows.png)
![Sign In](./screenshots/signin.png)
## Development
The software is made up a Golang server, React webapp and a WireGuard
implementation that must be provided by the system.
The software is made up a Golang Server and React App.
Here's how I develop locally:
1. run `./dev-wg.sh` to get wireguard running locally on `:51820`
2. run `cd website && npm install && npm start` to get the frontend running on `:3000`
3. run `sudo go run ./main.go` to get the server running on `:8000`
Here are some notes about the development configuration:
- sudo is required because the server uses iptables/ip to configure the VPN networking
- you'll access the website on `:3000` and it'll proxy API requests to `:8000` thanks to webpack dev proxy
- because we haven't configured a WIREGUARD_PRIVATE_KEY the server will generate one in-memory
- similarly we didn't configure a STORAGE_DIRECTORY so the server will store client config in-memory
- you'll access the website on `:3000` and it'll proxy API requests to `:8000` thanks to webpack
- in-memory storage and generated wireguard keys will be used
GRPC codegeneration:
The client communicates with the server via gRPC-Web. You can edit the API specification
in `./proto/*.proto`.
After changing a service or message definition you'll want to re-generate server and client
code using: `./codegen.sh`.

@ -11,3 +11,5 @@ protoc \
-I proto/ \
proto/*.proto \
--go_out="plugins=grpc:$OUT_DIR"
cd website && npm run codegen

@ -1,32 +0,0 @@
#!/bin/bash
# This script will build the Dockerfile
# and then run it with a minimalistic set of
# docker run arguments
#
# note that "WIREGUARD_PRIVATE_KEY" used in
# this configuration is for the demo and clearly
# not secure, please don't copy-paste it
set -eo pipefail
if [[ -z $1 ]]; then
echo "USAGE: $0 <path-to-config-file>"
exit 1
fi
CONFIG_FILE="$1"
docker build -t place1/wg-access-server .
docker run \
-it \
--rm \
--name wg \
--cap-add NET_ADMIN \
--device /dev/net/tun:/dev/net/tun \
-v "$CONFIG_FILE:/config.yaml" \
-v demo-data:/data \
-e "LOG_LEVEL=Debug" \
-p 8000:8000/tcp \
-p 51820:51820/udp \
-p 53:53/udp \
place1/wg-access-server /server --config /config.yaml

@ -1,14 +0,0 @@
#!/bin/bash
set -eou pipefail
docker build -t dev-wg --target boringtun .
docker run \
--rm \
-it \
--network host \
--device /dev/net/tun:/dev/net/tun \
--cap-add NET_ADMIN \
-v /var/run/wireguard:/var/run/wireguard \
dev-wg \
boringtun wg0 --disable-drop-privileges=root --foreground --verbosity=debug

@ -8,12 +8,15 @@ require (
github.com/coreos/go-iptables v0.4.3
github.com/coreos/go-oidc v2.2.1+incompatible
github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect
github.com/docker/docker v1.13.1 // indirect
github.com/docker/libnetwork v0.8.0-dev.2.0.20200217033114-6659f7f4d8c1
github.com/golang/protobuf v1.3.3
github.com/gorilla/mux v1.7.4
github.com/gorilla/sessions v1.2.0
github.com/gorilla/websocket v1.4.1 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.2.0
github.com/improbable-eng/grpc-web v0.12.0
github.com/ishidawataru/sctp v0.0.0-20191218070446-00ab2ac2db07 // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/miekg/dns v1.1.27

@ -3,6 +3,7 @@ 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=
@ -18,6 +19,10 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f h1:U5y3Y5UE0w7amNe7Z5G/twsBW0KEalRQXZzf8ufSh9I=
github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f/go.mod h1:xH/i4TFMt8koVQZ6WFms69WAsDWr2XsYL3Hkl7jkoLE=
github.com/docker/docker v1.13.1 h1:IkZjBSIc8hBjLpqeAbeE5mca5mNgeatLHBy3GO78BWo=
github.com/docker/docker v1.13.1/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/libnetwork v0.8.0-dev.2.0.20200217033114-6659f7f4d8c1 h1:Y1inpcbXnwGzRxGAwp7Hduv68mbKo8IDO9/w1KaHqQQ=
github.com/docker/libnetwork v0.8.0-dev.2.0.20200217033114-6659f7f4d8c1/go.mod h1:93m0aTqz6z+g32wla4l4WxTrdtvBRmVzYRkYvasA5Z8=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@ -52,6 +57,8 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.2.0 h1:0IKlLyQ3Hs9nDaiK5cSHAGmcQ
github.com/grpc-ecosystem/go-grpc-middleware v1.2.0/go.mod h1:mJzapYve32yjrKlk9GbyCZHuPgZsrbyIbyKhSzOpg6s=
github.com/improbable-eng/grpc-web v0.12.0 h1:GlCS+lMZzIkfouf7CNqY+qqpowdKuJLSLLcKVfM1oLc=
github.com/improbable-eng/grpc-web v0.12.0/go.mod h1:6hRR09jOEG81ADP5wCQju1z71g6OL4eEvELdran/3cs=
github.com/ishidawataru/sctp v0.0.0-20191218070446-00ab2ac2db07 h1:rw3IAne6CDuVFlZbPOkA7bhxlqawFh7RJJ+CejfMaxE=
github.com/ishidawataru/sctp v0.0.0-20191218070446-00ab2ac2db07/go.mod h1:co9pwDoBCm1kGxawmb4sPq0cSIOOWNPT4KnHotMP1Zg=
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a h1:84IpUNXj4mCR9CuCEvSiCArMbzr/TMbuPIadKDwypkI=
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=

@ -21,18 +21,7 @@ import (
type AppConfig struct {
LogLevel string `yaml:"loglevel"`
Web struct {
// ExternalAddress is that users access the web ui
// using. This value is required for using auth backends
// This value should include the scheme.
// The port should be included if non-standard.
// e.g. http://192.168.0.2:8000
// or https://myvpn.example.com
ExternalAddress string `yaml:"externalAddress"`
// Port that the web server should listen on
Port int `yaml:"port"`
} `yaml:"web"`
Storage struct {
Storage struct {
// Directory that VPN devices (WireGuard peers)
// should be saved under.
// If this value is empty then an InMemory storage
@ -52,12 +41,13 @@ type AppConfig struct {
// their connection configuration or setup
// their VPN again using the web ui (easier for most people)
PrivateKey string `yaml:"privateKey"`
// ExternalAddress is the address that users
// ExternalAddress is the address that clients
// use to connect to the wireguard interface
// By default, this will use the Web.ExternalAddress
// domain with the WireGuard.Port
ExternalAddress *string `yaml:"externalAddress"`
// By default, this will be empty and the web ui
// will use the current page's origin.
ExternalHost *string `yaml:"externalHost"`
// The WireGuard ListenPort
// Defaults to 51820
Port int `yaml:"port"`
} `yaml:"wireguard"`
VPN struct {
@ -71,10 +61,12 @@ type AppConfig struct {
// to the outside internet
GatewayInterface string `yaml:"gatewayInterface"`
}
DNS struct {
// TODO: docs
Upstream []string `yaml:"upstream"`
} `yaml:"dns"`
// 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.
Auth *authconfig.AuthConfig `yaml:"auth"`
}
@ -88,18 +80,15 @@ func Read() *AppConfig {
config := AppConfig{}
config.LogLevel = "info"
config.Web.Port = 8000
config.WireGuard.InterfaceName = "wg0"
config.WireGuard.Port = 51820
config.VPN.CIDR = "10.44.0.0/24"
if *configPath != "" {
b, err := ioutil.ReadFile(*configPath)
if err != nil {
logrus.Fatal(errors.Wrap(err, "failed to read the configuration file"))
}
if err := yaml.Unmarshal(b, &config); err != nil {
logrus.Fatal(errors.Wrap(err, "failed to bind configuration file"))
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"))
}
}
}

@ -4,8 +4,11 @@ import (
"fmt"
"net"
"runtime/debug"
"strings"
"time"
"github.com/docker/libnetwork/resolvconf"
"github.com/docker/libnetwork/types"
"github.com/miekg/dns"
"github.com/patrickmn/go-cache"
"github.com/pkg/errors"
@ -19,12 +22,20 @@ type DNSServer struct {
upstream []string
}
func New(upstream []string) (*DNSServer, error) {
func New() (*DNSServer, error) {
upstream := []string{}
if r, err := resolvconf.Get(); err == nil {
upstream = resolvconf.GetNameservers(r.Content, types.IPv4)
}
if len(upstream) == 0 {
upstream = []string{"1.1.1.1"}
logrus.Warn("failed to get nameservers from /etc/resolv.conf defaulting to 1.1.1.1 for DNS instead")
upstream = append(upstream, "1.1.1.1")
}
logrus.Infof("starting dns server with upstreams: %v", upstream)
logrus.Infof("starting dns server with upstreams: %s", strings.Join(upstream, ", "))
dnsServer := &DNSServer{
server: &dns.Server{

@ -27,16 +27,10 @@ func (s *ServerService) Info(ctx context.Context, req *proto.InfoReq) (*proto.In
return nil, status.Errorf(codes.Internal, "failed to get public key")
}
port, err := wgembed.Port(s.Config.WireGuard.InterfaceName)
if err != nil {
logrus.Error(err)
return nil, status.Errorf(codes.Internal, "failed to get port")
}
return &proto.InfoRes{
Host: stringValue(s.Config.WireGuard.ExternalAddress),
Host: stringValue(s.Config.WireGuard.ExternalHost),
PublicKey: publicKey,
Port: int32(port),
Port: int32(s.Config.WireGuard.Port),
HostVpnIp: ServerVPNIP(s.Config.VPN.CIDR).IP.String(),
}, nil
}

@ -2,7 +2,6 @@ package main
import (
"crypto/rand"
"fmt"
"math"
"net/http"
"runtime/debug"
@ -64,7 +63,7 @@ func main() {
}
// DNS Server
dns, err := dnsproxy.New(conf.DNS.Upstream)
dns, err := dnsproxy.New()
if err != nil {
logrus.Fatal(errors.Wrap(err, "failed to start dns server"))
}
@ -136,7 +135,7 @@ func main() {
}
// Listen
address := fmt.Sprintf("0.0.0.0:%d", conf.Web.Port)
address := "0.0.0.0:8000"
srv := &http.Server{
Addr: address,
Handler: handler,

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Loading…
Cancel
Save