initial commit
commit
c40f7971c3
@ -0,0 +1,3 @@
|
||||
.git/
|
||||
**/node_modules/
|
||||
**/build/
|
@ -0,0 +1,38 @@
|
||||
FROM rust:1.38.0-alpine as boringtun
|
||||
RUN apk add musl-dev
|
||||
RUN cargo install \
|
||||
--root / \
|
||||
--bin boringtun \
|
||||
--git https://github.com/cloudflare/boringtun.git \
|
||||
--rev 0b980a2f5a5f8622bf0e3a024ace63ad01b5d0f6
|
||||
|
||||
FROM node:10 as website
|
||||
WORKDIR /code
|
||||
COPY ./website/package.json ./
|
||||
COPY ./website/package-lock.json ./
|
||||
RUN npm install
|
||||
COPY ./website/ ./
|
||||
RUN npm run build
|
||||
|
||||
FROM golang:1.13 as server
|
||||
WORKDIR /code
|
||||
ENV GOOS=linux
|
||||
ENV GARCH=amd64
|
||||
ENV CGO_ENABLED=0
|
||||
ENV GO111MODULE=on
|
||||
COPY ./go.mod ./
|
||||
COPY ./go.sum ./
|
||||
RUN go mod download
|
||||
COPY ./main.go ./main.go
|
||||
COPY ./internal/ ./internal
|
||||
RUN go build -o server
|
||||
|
||||
FROM alpine:3.10
|
||||
RUN apk add iptables
|
||||
RUN apk add wireguard-tools
|
||||
ENV WIREGUARD_USERSPACE_IMPLEMENTATION=boringtun
|
||||
ENV STORAGE_DIRECTORY="/data"
|
||||
COPY --from=boringtun /bin/boringtun /usr/local/bin/boringtun
|
||||
COPY --from=server /code/server ./server
|
||||
COPY --from=website /code/build ./website/build
|
||||
CMD ./server
|
@ -0,0 +1,26 @@
|
||||
#!/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 -eou pipefail
|
||||
|
||||
docker build -t demo .
|
||||
|
||||
read -p "Enter LAN ip address i.e. 192.168.0.2 : " external_address
|
||||
|
||||
docker run \
|
||||
-it \
|
||||
--rm \
|
||||
--name wg \
|
||||
--cap-add NET_ADMIN \
|
||||
--device /dev/net/tun:/dev/net/tun \
|
||||
-v wgdata:/data \
|
||||
-p 8000:8000/tcp \
|
||||
-p 51820:51820/udp \
|
||||
-e WIREGUARD_PRIVATE_KEY="kH4F1lldSzgEMB7wfQ1ccujAhZCCCCEeh2Kvhxf+XFw=" \
|
||||
-e WEB_EXTERNAL_ADDRESS="$external_address" \
|
||||
demo
|
@ -0,0 +1,16 @@
|
||||
module github.com/place1/wireguard-access-server
|
||||
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc // indirect
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect
|
||||
github.com/coreos/go-iptables v0.4.3
|
||||
github.com/gorilla/mux v1.7.3
|
||||
github.com/pkg/errors v0.8.1
|
||||
github.com/sirupsen/logrus v1.4.2
|
||||
github.com/vishvananda/netlink v1.0.0
|
||||
github.com/vishvananda/netns v0.0.0-20190625233234-7109fa855b0f // indirect
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20191008142428-8d021180e987
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
||||
)
|
@ -0,0 +1,68 @@
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/coreos/go-iptables v0.4.3 h1:jJg1aFuhCqWbgBl1VTqgTHG5faPM60A5JDMjQ2HYv+A=
|
||||
github.com/coreos/go-iptables v0.4.3/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU=
|
||||
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/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
|
||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
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/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/mdlayher/genetlink v0.0.0-20191004171646-5cf585d3b847 h1:EFRfaQaWMFsAqLGDvz9jYIlcMImQFCnCmohvVdVgdY8=
|
||||
github.com/mdlayher/genetlink v0.0.0-20191004171646-5cf585d3b847/go.mod h1:LNhNWFVJapYK8zEjVHUIle4gy+Oahfc3UtcaqZ8Dz98=
|
||||
github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
|
||||
github.com/mdlayher/netlink v0.0.0-20191004170026-3c8695cb0643/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
|
||||
github.com/mdlayher/netlink v0.0.0-20191008140946-2a17fd90af51 h1:rP02cBlv8sk9kC1iRINOapZNB9B5S6JChwmYXDiFKpU=
|
||||
github.com/mdlayher/netlink v0.0.0-20191008140946-2a17fd90af51/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
|
||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
|
||||
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/vishvananda/netlink v1.0.0 h1:bqNY2lgheFIu1meHUFSH3d7vG93AFyqg3oGbJCOJgSM=
|
||||
github.com/vishvananda/netlink v1.0.0/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
|
||||
github.com/vishvananda/netns v0.0.0-20190625233234-7109fa855b0f h1:nBX3nTcmxEtHSERBJaIo1Qa26VwRaopnZmfDQUXsF4I=
|
||||
github.com/vishvananda/netns v0.0.0-20190625233234-7109fa855b0f/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc h1:c0o/qxkaO2LF5t6fQrT4b5hzyggAkLLlCUjqfRxd8Q4=
|
||||
golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191003171128-d98b1b443823/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191007182048-72f939374954 h1:JGZucVF/L/TotR719NbujzadOZ2AgnYlqphQGHDCKaU=
|
||||
golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190830023255-19e00faab6ad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191003212358-c178f38b412c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be h1:QAcqgptGM8IQBC9K/RC4o+O9YmqEm0diQn9QmZw/0mU=
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.zx2c4.com/wireguard v0.0.20190908 h1:SUoXDdwSMtomLdvke+zz83/u9tNvl4hHmcTIWp38tow=
|
||||
golang.zx2c4.com/wireguard v0.0.20190908/go.mod h1:LhfXh5z6bLC2lW2ve6BzYZFwnnsXK3OQjySR0Yh2dO8=
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20191008142428-8d021180e987 h1:26OAgqBTufVr8WKonCEhhjO1oKsYhHv0iM5Dg92G1TM=
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20191008142428-8d021180e987/go.mod h1:7hq1rEDsx7/FWl8IEEnfH2Xhs6M2MNnjUfN0PeI8Rm0=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
@ -0,0 +1,184 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
|
||||
type AppConfig struct {
|
||||
Web struct {
|
||||
// ExternalAddress is the address that
|
||||
// clients should use to connect to this
|
||||
// server. It will be used in generated
|
||||
// VPN client connection configuration
|
||||
// ExternalAddress should not include any
|
||||
// port infomation.
|
||||
// The WireGuard port will be appended.
|
||||
ExternalAddress string
|
||||
// Port that the web server should listen on
|
||||
Port int
|
||||
}
|
||||
Storage struct {
|
||||
// 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 string
|
||||
}
|
||||
WireGuard struct {
|
||||
// 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
|
||||
UserspaceImplementation string
|
||||
// The network interface name of the WireGuard
|
||||
// network device
|
||||
InterfaceName string
|
||||
// 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)
|
||||
PrivateKey string
|
||||
// The WireGuard ListenPort
|
||||
Port int
|
||||
}
|
||||
VPN struct {
|
||||
// SubnetCIDR configures a network address space
|
||||
// that client (WireGuard peers) will be allocated
|
||||
// an IP address from
|
||||
SubnetCIDR string
|
||||
// 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
|
||||
GatewayInterface netlink.Link
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
app = kingpin.New("TODO: name-this-program", "An all-in-one WireGuard VPN solution")
|
||||
logLevel = app.Flag("loglevel", "Enable debug mode").Default("info").OverrideDefaultFromEnvar("LOG_LEVEL").String()
|
||||
webPort = app.Flag("web-port", "The web server port").Default("8000").OverrideDefaultFromEnvar("WEB_PORT").Int()
|
||||
webExternalAddress = app.Flag("web-external-address", "The external address that the service is accessible from excluding any scheme or port (e.g. vpn.example.com). Defaults to the IP address of your default interface").OverrideDefaultFromEnvar("WEB_EXTERNAL_ADDRESS").String()
|
||||
storageDirectory = app.Flag("storage-directory", "The directory where vpn devices (i.e. peers) will be stored").OverrideDefaultFromEnvar("STORAGE_DIRECTORY").String()
|
||||
wireGuardUserspaceImplementation = app.Flag("wireguard-userspace-implementation", "The a userspace implementation of wireguard e.g. wireguard-go or boringtun").OverrideDefaultFromEnvar("WIREGUARD_USERSPACE_IMPLEMENTATION").String()
|
||||
wireGuardInterfaceName = app.Flag("wireguard-interface-name", "The name of the WireGuard interface").Default("wg0").OverrideDefaultFromEnvar("WIREGUARD_INTERFACE_NAME").String()
|
||||
wireguardPort = app.Flag("wireguard-port", "The WireGuard ListenPort").Default("51820").OverrideDefaultFromEnvar("WIREGUARD_PORT").Int()
|
||||
wireguardPrivateKey = app.Flag("wireguard-private-key", "The WireGuard private key").OverrideDefaultFromEnvar("WIREGUARD_PRIVATE_KEY").String()
|
||||
vpnGatewayInterfaceName = app.Flag("vpn-gateway-interface-name", "The name of the network interface you want VPN client traffic to foward to").OverrideDefaultFromEnvar("VPN_GATEWAY_INTERFACE_NAME").String()
|
||||
vpnSubnetCIDR = app.Flag("vpn-subnet-cidr", "The subnet CIDR that clients should be networked within").Default("10.44.0.1/24").OverrideDefaultFromEnvar("VPN_SUBNET_CIDR").String()
|
||||
)
|
||||
|
||||
func Read() *AppConfig {
|
||||
kingpin.MustParse(app.Parse(os.Args[1:]))
|
||||
config := AppConfig{}
|
||||
|
||||
config.Web.Port = *webPort
|
||||
config.Web.ExternalAddress = *webExternalAddress
|
||||
config.Storage.Directory = *storageDirectory
|
||||
config.WireGuard.UserspaceImplementation = *wireGuardUserspaceImplementation
|
||||
config.WireGuard.InterfaceName = *wireGuardInterfaceName
|
||||
config.WireGuard.Port = *wireguardPort
|
||||
config.WireGuard.PrivateKey = *wireguardPrivateKey
|
||||
config.VPN.SubnetCIDR = *vpnSubnetCIDR
|
||||
config.VPN.GatewayInterface = findGatewayLink(*vpnGatewayInterfaceName)
|
||||
if config.Web.ExternalAddress == "" && config.VPN.GatewayInterface != nil {
|
||||
if ip, err := linkIPAddr(config.VPN.GatewayInterface); err == nil {
|
||||
config.Web.ExternalAddress = ip.String()
|
||||
logrus.Infof("no external address was configured - using %s from the gateway interface", config.Web.ExternalAddress)
|
||||
}
|
||||
}
|
||||
|
||||
level, err := logrus.ParseLevel(*logLevel)
|
||||
if err != nil {
|
||||
logrus.Fatal(errors.Wrap(err, "invalid log level - should be one of fatal, error, warn, info, debug, trace"))
|
||||
}
|
||||
logrus.SetLevel(level)
|
||||
logrus.SetReportCaller(true)
|
||||
logrus.SetFormatter(&logrus.TextFormatter{
|
||||
CallerPrettyfier: func(f *runtime.Frame) (string, string) {
|
||||
return "", fmt.Sprintf("%s:%d", filepath.Base(f.File), f.Line)
|
||||
},
|
||||
})
|
||||
|
||||
if config.WireGuard.PrivateKey == "" {
|
||||
logrus.Warn("no private key has been configured! using an in-memory private key that will be lost when the process exits!")
|
||||
key, err := wgtypes.GeneratePrivateKey()
|
||||
if err != nil {
|
||||
logrus.Fatal(errors.Wrap(err, "failed to generate a server private key"))
|
||||
}
|
||||
config.WireGuard.PrivateKey = key.String()
|
||||
}
|
||||
|
||||
if config.Storage.Directory == "" {
|
||||
logrus.Warn("storage directory not configured - using in-memory storage backend! wireguard devices will be lost when the process exits!")
|
||||
}
|
||||
|
||||
return &config
|
||||
}
|
||||
|
||||
func defaultInterface() string {
|
||||
links, err := netlink.LinkList()
|
||||
if err != nil {
|
||||
logrus.Warn(errors.Wrap(err, "failed to list network interfaces"))
|
||||
return ""
|
||||
}
|
||||
for _, link := range links {
|
||||
routes, err := netlink.RouteList(link, 4)
|
||||
if err != nil {
|
||||
logrus.Warn(errors.Wrapf(err, "failed to list routes for interface %s", link.Attrs().Name))
|
||||
return ""
|
||||
}
|
||||
for _, route := range routes {
|
||||
if route.Dst == nil {
|
||||
return link.Attrs().Name
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func findGatewayLink(name string) netlink.Link {
|
||||
if name == "" {
|
||||
if name = defaultInterface(); name == "" {
|
||||
logrus.Warn("a gateway interface name was not configured - vpn forwarding rules will not be applied!")
|
||||
return nil
|
||||
} else {
|
||||
logrus.Infof("no gateway interface name was configured - using the system's default route's interface %s", name)
|
||||
}
|
||||
}
|
||||
if name == "" {
|
||||
}
|
||||
link, err := netlink.LinkByName(name)
|
||||
if err != nil {
|
||||
logrus.Warn(errors.Wrapf(err, "the gateway interface '%s' could not be found - vpn forwarding rules will not be applied!", name))
|
||||
return nil
|
||||
}
|
||||
return link
|
||||
}
|
||||
|
||||
func linkIPAddr(link netlink.Link) (net.IP, error) {
|
||||
routes, err := netlink.RouteList(link, 4)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to list routes for interface %s", link.Attrs().Name)
|
||||
}
|
||||
for _, route := range routes {
|
||||
if route.Src != nil {
|
||||
return route.Src, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no source IP found for interface %s", link.Attrs().Name)
|
||||
}
|
@ -0,0 +1,167 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/place1/wireguard-access-server/internal/storage"
|
||||
"github.com/place1/wireguard-access-server/internal/wg"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var vpnip, vpnsubnet = MustParseCIDR("10.0.0.1/24")
|
||||
|
||||
var peerid = 1
|
||||
|
||||
// we need to give each device (i.e. wg peer)
|
||||
// an ip within the VPN's subnet
|
||||
// idk how i'm going to maintain this infomation just yet
|
||||
func nextPeerID() int {
|
||||
peerid = peerid + 1
|
||||
return peerid
|
||||
}
|
||||
|
||||
type DeviceManager struct {
|
||||
wgserver *wg.Server
|
||||
storage storage.Storage
|
||||
}
|
||||
|
||||
func NewDeviceManager(w *wg.Server, s storage.Storage) *DeviceManager {
|
||||
return &DeviceManager{w, s}
|
||||
}
|
||||
|
||||
func (d *DeviceManager) Sync() error {
|
||||
devices, err := d.ListDevices()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to list devices")
|
||||
}
|
||||
for _, device := range devices {
|
||||
if err := d.wgserver.AddPeer(device.PublicKey, device.Address); err != nil {
|
||||
logrus.Warn(errors.Wrapf(err, "failed to sync device '%s' (ignoring)", device.Name))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DeviceManager) AddDevice(name string, publicKey string) (*storage.Device, error) {
|
||||
|
||||
if name == "" {
|
||||
return nil, errors.New("device name must not be empty")
|
||||
}
|
||||
|
||||
clientAddr, err := d.nextClientAddress()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to generate an ip address for device")
|
||||
}
|
||||
|
||||
device := &storage.Device{
|
||||
Name: name,
|
||||
PublicKey: publicKey,
|
||||
Endpoint: d.wgserver.Endpoint(),
|
||||
Address: clientAddr,
|
||||
DNS: d.wgserver.DNS(),
|
||||
CreatedAt: time.Now(),
|
||||
ServerPublicKey: d.wgserver.PublicKey(),
|
||||
}
|
||||
|
||||
if err := d.storage.Save(device); err != nil {
|
||||
// TODO: might need to clean up the wg config?
|
||||
// might need to save before adding to wg?
|
||||
// idk lol
|
||||
return nil, errors.Wrap(err, "failed to save the new device")
|
||||
}
|
||||
|
||||
if err := d.wgserver.AddPeer(publicKey, clientAddr); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to provision peer")
|
||||
}
|
||||
|
||||
return device, nil
|
||||
}
|
||||
|
||||
func (d *DeviceManager) ListDevices() ([]*storage.Device, error) {
|
||||
return d.storage.List()
|
||||
}
|
||||
|
||||
func (d *DeviceManager) DeleteDevice(name string) error {
|
||||
device, err := d.storage.Get(name)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to retrieve device")
|
||||
}
|
||||
if err := d.storage.Delete(device); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.wgserver.RemovePeer(device.PublicKey); err != nil {
|
||||
return errors.Wrap(err, "device was removed from storage but failed to be removed from the wireguard interface")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var nextIPLock = sync.Mutex{}
|
||||
|
||||
func (d *DeviceManager) nextClientAddress() (string, error) {
|
||||
nextIPLock.Lock()
|
||||
defer nextIPLock.Unlock()
|
||||
|
||||
devices, err := d.ListDevices()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to list devices")
|
||||
}
|
||||
|
||||
// TODO: read up on better ways to allocate client's IP
|
||||
// addresses from a configurable CIDR
|
||||
usedIPs := []net.IP{
|
||||
MustParseIP("10.0.0.0"),
|
||||
MustParseIP("10.0.0.1"),
|
||||
MustParseIP("10.0.0.255"),
|
||||
}
|
||||
for _, device := range devices {
|
||||
ip, _ := MustParseCIDR(device.Address)
|
||||
usedIPs = append(usedIPs, ip)
|
||||
}
|
||||
|
||||
ip := vpnip
|
||||
for ip := ip.Mask(vpnsubnet.Mask); vpnsubnet.Contains(ip); ip = nextIP(ip) {
|
||||
if !contains(usedIPs, ip) {
|
||||
return fmt.Sprintf("%s/32", ip.String()), nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("there are no free IP addresses in the vpn subnet: '%s'", vpnsubnet)
|
||||
}
|
||||
|
||||
func contains(ips []net.IP, target net.IP) bool {
|
||||
for _, ip := range ips {
|
||||
if ip.Equal(target) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
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,22 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Storage interface {
|
||||
Save(*Device) error
|
||||
List() ([]*Device, error)
|
||||
Get(string) (*Device, error)
|
||||
Delete(*Device) error
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
Name string `json:"name"`
|
||||
PublicKey string `json:"publicKey"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Address string `json:"address"`
|
||||
DNS string `json:"dns"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
ServerPublicKey string `json:"serverPublicKey"`
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// implements Storage interface
|
||||
type DiskStorage struct {
|
||||
directory string
|
||||
}
|
||||
|
||||
func NewDiskStorage(directory string) *DiskStorage {
|
||||
if _, err := os.Stat(directory); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(directory, 0600); err != nil {
|
||||
logrus.Fatal(errors.Wrap(err, "failed to create storage directory"))
|
||||
}
|
||||
}
|
||||
return &DiskStorage{directory}
|
||||
}
|
||||
|
||||
func (s *DiskStorage) Save(device *Device) error {
|
||||
path := s.deviceFilePath(device.Name)
|
||||
logrus.Infof("saving new device %s", path)
|
||||
bytes, err := json.Marshal(device)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to marshal device")
|
||||
}
|
||||
if err := ioutil.WriteFile(path, bytes, 0600); err != nil {
|
||||
return errors.Wrapf(err, "failed to write device to file %s", path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DiskStorage) List() ([]*Device, error) {
|
||||
devices := []*Device{}
|
||||
files, err := ioutil.ReadDir(s.directory)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to list storage directory")
|
||||
}
|
||||
for _, file := range files {
|
||||
device, err := s.Get(filepath.Base(strings.TrimSuffix(file.Name(), filepath.Ext(file.Name()))))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to read device file")
|
||||
}
|
||||
devices = append(devices, device)
|
||||
}
|
||||
return devices, nil
|
||||
}
|
||||
|
||||
func (s *DiskStorage) Get(name string) (*Device, error) {
|
||||
path := s.deviceFilePath(name)
|
||||
bytes, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to read device file %s", path)
|
||||
}
|
||||
device := &Device{}
|
||||
if err := json.Unmarshal(bytes, device); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to unmarshal device file %s", path)
|
||||
}
|
||||
return device, nil
|
||||
}
|
||||
|
||||
func (s *DiskStorage) Delete(device *Device) error {
|
||||
if err := os.Remove(s.deviceFilePath(device.Name)); err != nil {
|
||||
return errors.Wrap(err, "failed to delete device file")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DiskStorage) deviceFilePath(name string) string {
|
||||
// TODO: protect against path traversal
|
||||
// and make sure names are reasonably sane
|
||||
return filepath.Join(s.directory, fmt.Sprintf("%s.json", name))
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package storage
|
||||
|
||||
import "errors"
|
||||
|
||||
// implements Storage interface
|
||||
type InMemoryStorage struct{}
|
||||
|
||||
var memory = map[string]*Device{}
|
||||
|
||||
func NewMemoryStorage() *InMemoryStorage {
|
||||
return &InMemoryStorage{}
|
||||
}
|
||||
|
||||
func (s *InMemoryStorage) Save(device *Device) error {
|
||||
memory[device.Name] = device
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *InMemoryStorage) List() ([]*Device, error) {
|
||||
devices := []*Device{}
|
||||
for _, device := range memory {
|
||||
devices = append(devices, device)
|
||||
}
|
||||
return devices, nil
|
||||
}
|
||||
|
||||
func (s *InMemoryStorage) Get(name string) (*Device, error) {
|
||||
device, ok := memory[name]
|
||||
if !ok {
|
||||
return nil, errors.New("device doesn't exist")
|
||||
}
|
||||
return device, nil
|
||||
}
|
||||
|
||||
func (s *InMemoryStorage) Delete(device *Device) error {
|
||||
delete(memory, device.Name)
|
||||
return nil
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/place1/wireguard-access-server/internal/services"
|
||||
"github.com/place1/wireguard-access-server/internal/storage"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type AddDeviceRequest struct {
|
||||
Name string `json:"name"`
|
||||
PublicKey string `json:"publicKey"`
|
||||
}
|
||||
|
||||
type AddDeviceResponse struct {
|
||||
Device *storage.Device `json:"device"`
|
||||
}
|
||||
|
||||
func AddDevice(devices *services.DeviceManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
req := AddDeviceRequest{}
|
||||
if err := decoder.Decode(&req); err != nil {
|
||||
logrus.Error(errors.Wrap(err, "unable to decode request body"))
|
||||
http.Error(w, "bad request payload", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
device, err := devices.AddDevice(req.Name, req.PublicKey)
|
||||
if err != nil {
|
||||
logrus.Error(errors.Wrap(err, "unable to add device"))
|
||||
http.Error(w, "failed to add the new device", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response, err := json.Marshal(AddDeviceResponse{
|
||||
Device: device,
|
||||
})
|
||||
if err != nil {
|
||||
logrus.Error(errors.Wrap(err, "failed to marshal response"))
|
||||
http.Error(w, "failed to marshal response", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(response)
|
||||
return
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/place1/wireguard-access-server/internal/services"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func DeleteDevice(devices *services.DeviceManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
name, ok := vars["name"]
|
||||
if !ok {
|
||||
http.Error(w, "missing device name in path", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := devices.DeleteDevice(name); err != nil {
|
||||
logrus.Error(errors.Wrap(err, "failed to remove device"))
|
||||
http.Error(w, "failed to remove device", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/place1/wireguard-access-server/internal/services"
|
||||
"github.com/place1/wireguard-access-server/internal/storage"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type ListDeviceRequest struct{}
|
||||
|
||||
type ListDeviceResponse struct {
|
||||
Items []*storage.Device `json:"items"`
|
||||
}
|
||||
|
||||
func ListDevices(devices *services.DeviceManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
devices, err := devices.ListDevices()
|
||||
if err != nil {
|
||||
logrus.Error(errors.Wrap(err, "failed to list devices"))
|
||||
http.Error(w, "failed to list devices", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response, err := json.Marshal(ListDeviceResponse{
|
||||
Items: devices,
|
||||
})
|
||||
if err != nil {
|
||||
logrus.Error(errors.Wrap(err, "failed to marshal response"))
|
||||
http.Error(w, "failed to marshal response", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(response)
|
||||
return
|
||||
}
|
||||
}
|
@ -0,0 +1,174 @@
|
||||
package wg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.zx2c4.com/wireguard/wgctrl"
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
client *wgctrl.Client
|
||||
iface string
|
||||
externalName string
|
||||
port int
|
||||
publicKey wgtypes.Key
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func New(iface string, privateKey string, port int, externalName string) (*Server, error) {
|
||||
// wgctrl.New() will search for a kernel implementation
|
||||
// of wireguard, then user implementations
|
||||
// user implementations are found in /var/run/wireguard/<iface>.sock
|
||||
// this unix socket likely requires root to access
|
||||
client, err := wgctrl.New()
|
||||
if err != nil {
|
||||
logrus.Fatal(errors.Wrap(err, "failed to create wgctrl"))
|
||||
}
|
||||
key, err := wgtypes.ParseKey(privateKey)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "bad private key format")
|
||||
}
|
||||
server := &Server{
|
||||
client: client,
|
||||
iface: iface,
|
||||
port: port,
|
||||
externalName: externalName,
|
||||
publicKey: key.PublicKey(),
|
||||
}
|
||||
err = server.configure(func(config *wgtypes.Config) error {
|
||||
config.PrivateKey = &key
|
||||
config.ListenPort = &port
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to configure wireguard - is wireguard running?")
|
||||
}
|
||||
return server, nil
|
||||
}
|
||||
|
||||
func (s *Server) AddPeer(publicKey string, addressCIDR string) error {
|
||||
logrus.
|
||||
WithField("publicKey", publicKey).
|
||||
WithField("address", addressCIDR).
|
||||
Debugf("adding peer")
|
||||
key, err := wgtypes.ParseKey(publicKey)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "bad public key %v", publicKey)
|
||||
}
|
||||
_, allowedIPs, err := net.ParseCIDR(addressCIDR)
|
||||
if err != nil || allowedIPs == nil {
|
||||
return errors.Wrap(err, "bad CIDR value for AllowedIPs")
|
||||
}
|
||||
if s.HasPeer(key.String()) {
|
||||
s.RemovePeer(key.String())
|
||||
}
|
||||
return s.configure(func(config *wgtypes.Config) error {
|
||||
config.ReplacePeers = false
|
||||
config.Peers = []wgtypes.PeerConfig{
|
||||
wgtypes.PeerConfig{
|
||||
PublicKey: key,
|
||||
AllowedIPs: []net.IPNet{*allowedIPs},
|
||||
},
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) ListPeers() ([]wgtypes.Peer, error) {
|
||||
d, err := s.Device()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.Peers, nil
|
||||
}
|
||||
|
||||
func (s *Server) Peer(publicKey string) (*wgtypes.Peer, error) {
|
||||
peers, err := s.ListPeers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, peer := range peers {
|
||||
if peer.PublicKey.String() == publicKey {
|
||||
return &peer, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("peer with public key '%s' not found", publicKey)
|
||||
}
|
||||
|
||||
func (s *Server) HasPeer(publicKey string) bool {
|
||||
peers, err := s.ListPeers()
|
||||
if err != nil {
|
||||
logrus.Error(errors.Wrap(err, "failed to list peers"))
|
||||
return false
|
||||
}
|
||||
for _, peer := range peers {
|
||||
if peer.PublicKey.String() == publicKey {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Server) RemovePeer(publicKey string) error {
|
||||
logrus.WithField("publicKey", publicKey).Debug("removing peer")
|
||||
key, err := wgtypes.ParseKey(publicKey)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "bad public key")
|
||||
}
|
||||
return s.configure(func(config *wgtypes.Config) error {
|
||||
config.ReplacePeers = false
|
||||
config.Peers = []wgtypes.PeerConfig{
|
||||
wgtypes.PeerConfig{
|
||||
Remove: true,
|
||||
PublicKey: key,
|
||||
},
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) PublicKey() string {
|
||||
return s.publicKey.String()
|
||||
}
|
||||
|
||||
func (s *Server) Endpoint() string {
|
||||
return fmt.Sprintf("%s:%d", s.externalName, s.port)
|
||||
}
|
||||
|
||||
func (s *Server) DNS() string {
|
||||
return "1.1.1.1, 8.8.8.8" // TODO: dns stuff
|
||||
}
|
||||
|
||||
func (s *Server) Device() (*wgtypes.Device, error) {
|
||||
return s.client.Device(s.iface)
|
||||
}
|
||||
|
||||
func (s *Server) Close() error {
|
||||
return s.client.Close()
|
||||
}
|
||||
|
||||
func (s *Server) configure(cb func(*wgtypes.Config) error) error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
next := wgtypes.Config{}
|
||||
if err := cb(&next); err != nil {
|
||||
return errors.Wrap(err, "failed to get next wireguard config")
|
||||
} else {
|
||||
return s.client.ConfigureDevice(s.iface, next)
|
||||
}
|
||||
}
|
||||
|
||||
func trimLines(input string) string {
|
||||
lines := strings.Split(strings.TrimSpace(input), "\n")
|
||||
output := make([]string, len(lines))
|
||||
for index, line := range lines {
|
||||
output[index] = strings.TrimSpace(line)
|
||||
}
|
||||
return strings.Join(output, "\n")
|
||||
}
|
@ -0,0 +1,136 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-iptables/iptables"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/place1/wireguard-access-server/internal/config"
|
||||
"github.com/place1/wireguard-access-server/internal/services"
|
||||
"github.com/place1/wireguard-access-server/internal/storage"
|
||||
"github.com/place1/wireguard-access-server/internal/web"
|
||||
"github.com/place1/wireguard-access-server/internal/wg"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func main() {
|
||||
config := config.Read()
|
||||
|
||||
// Userspace wireguard command
|
||||
if config.WireGuard.UserspaceImplementation != "" {
|
||||
go func() {
|
||||
logrus.Infof("using userspace wireguard implementation %s", config.WireGuard.UserspaceImplementation)
|
||||
var command *exec.Cmd
|
||||
if config.WireGuard.UserspaceImplementation == "boringtun" {
|
||||
command = exec.Command(
|
||||
config.WireGuard.UserspaceImplementation,
|
||||
config.WireGuard.InterfaceName,
|
||||
"--disable-drop-privileges=root",
|
||||
"--foreground",
|
||||
)
|
||||
} else {
|
||||
command = exec.Command(
|
||||
config.WireGuard.UserspaceImplementation,
|
||||
"-f",
|
||||
config.WireGuard.InterfaceName,
|
||||
)
|
||||
}
|
||||
entry := logrus.NewEntry(logrus.New()).WithField("process", config.WireGuard.UserspaceImplementation)
|
||||
command.Stdout = entry.Writer()
|
||||
command.Stderr = entry.Writer()
|
||||
logrus.Infof("starting %s", command.String())
|
||||
if err := command.Run(); err != nil {
|
||||
logrus.Fatal(errors.Wrap(err, "userspace wireguard exitted"))
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for the userspace wireguard process to
|
||||
// startup and create the wg0 interface
|
||||
// Super sorry if this just caused a race
|
||||
// condition for you :(
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
// WireGuard
|
||||
wgserver, err := wg.New(
|
||||
config.WireGuard.InterfaceName,
|
||||
config.WireGuard.PrivateKey,
|
||||
config.WireGuard.Port,
|
||||
config.Web.ExternalAddress,
|
||||
)
|
||||
if err != nil {
|
||||
logrus.Fatal(errors.Wrap(err, "failed to create wgserver"))
|
||||
}
|
||||
defer wgserver.Close()
|
||||
logrus.Infof("wireguard server public key is %s", wgserver.PublicKey())
|
||||
logrus.Infof("wireguard endpoint is %s", wgserver.Endpoint())
|
||||
|
||||
// Networking configuration (ip links and route tables)
|
||||
link, err := netlink.LinkByName(config.WireGuard.InterfaceName)
|
||||
if err != nil {
|
||||
logrus.Fatal(errors.Wrap(err, "failed to find wireguard interface"))
|
||||
}
|
||||
addr, err := netlink.ParseAddr("10.0.0.1/24")
|
||||
if err != nil {
|
||||
logrus.Fatal(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"))
|
||||
}
|
||||
|
||||
// Networking configuration (iptables)
|
||||
if config.VPN.GatewayInterface != nil {
|
||||
ipt, err := iptables.New()
|
||||
if err != nil {
|
||||
logrus.Fatal(errors.Wrap(err, "failed to init iptables"))
|
||||
}
|
||||
if err := ipt.AppendUnique("filter", "FORWARD", "-s", "10.0.0.1/24", "-o", config.WireGuard.InterfaceName, "-j", "ACCEPT"); err != nil {
|
||||
logrus.Fatal(errors.Wrap(err, "failed to set ip tables rule"))
|
||||
}
|
||||
if err := ipt.AppendUnique("filter", "FORWARD", "-s", "10.0.0.1/24", "-i", config.WireGuard.InterfaceName, "-j", "ACCEPT"); err != nil {
|
||||
logrus.Fatal(errors.Wrap(err, "failed to set ip tables rule"))
|
||||
}
|
||||
if err := ipt.AppendUnique("nat", "POSTROUTING", "-s", "10.0.0.1/24", "-o", config.VPN.GatewayInterface.Attrs().Name, "-j", "MASQUERADE"); err != nil {
|
||||
logrus.Fatal(errors.Wrap(err, "failed to set ip tables rule"))
|
||||
}
|
||||
}
|
||||
|
||||
// Storage
|
||||
var storageDriver storage.Storage
|
||||
if config.Storage.Directory != "" {
|
||||
storageDriver = storage.NewDiskStorage(config.Storage.Directory)
|
||||
} else {
|
||||
storageDriver = storage.NewMemoryStorage()
|
||||
}
|
||||
|
||||
// Services
|
||||
deviceManager := services.NewDeviceManager(wgserver, storageDriver)
|
||||
if err := deviceManager.Sync(); err != nil {
|
||||
logrus.Fatal(errors.Wrap(err, "failed to sync"))
|
||||
}
|
||||
|
||||
// Router
|
||||
router := mux.NewRouter()
|
||||
router.HandleFunc("/api/devices", web.AddDevice(deviceManager)).Methods("POST")
|
||||
router.HandleFunc("/api/devices", web.ListDevices(deviceManager)).Methods("GET")
|
||||
router.HandleFunc("/api/devices/{name}", web.DeleteDevice(deviceManager)).Methods("DELETE")
|
||||
router.PathPrefix("/").Handler(http.FileServer(http.Dir("website/build")))
|
||||
|
||||
// Listen
|
||||
address := fmt.Sprintf("0.0.0.0:%d", config.Web.Port)
|
||||
logrus.Infof("website listening on %s", address)
|
||||
if err := http.ListenAndServe(address, router); err != nil {
|
||||
logrus.Fatal(errors.Wrap(err, "server exited"))
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "website",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4.5.1",
|
||||
"@material-ui/icons": "^4.5.1",
|
||||
"@types/jest": "24.0.19",
|
||||
"@types/node": "12.11.5",
|
||||
"@types/react": "16.9.9",
|
||||
"@types/react-dom": "16.9.2",
|
||||
"common-tags": "^1.8.0",
|
||||
"date-fns": "^2.6.0",
|
||||
"qrcode": "^1.4.2",
|
||||
"react": "^16.11.0",
|
||||
"react-dom": "^16.11.0",
|
||||
"react-easy-state": "^6.1.3",
|
||||
"react-scripts": "3.2.0",
|
||||
"tweetnacl-ts": "^1.0.3",
|
||||
"typeface-roboto": "0.0.75",
|
||||
"typescript": "3.6.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"proxy": "http://localhost:8000",
|
||||
"devDependencies": {
|
||||
"@types/common-tags": "^1.8.0",
|
||||
"@types/qrcode": "^1.3.4"
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
Binary file not shown.
After Width: | Height: | Size: 8.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
@ -0,0 +1,25 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
@ -0,0 +1,30 @@
|
||||
export enum Platform {
|
||||
Unknown,
|
||||
Mac,
|
||||
Ios,
|
||||
Windows,
|
||||
Android,
|
||||
Linux,
|
||||
}
|
||||
|
||||
// adapted from
|
||||
// https://stackoverflow.com/questions/38241480/detect-macos-ios-windows-android-and-linux-os-with-js
|
||||
export function getPlatform() {
|
||||
const userAgent = window.navigator.userAgent;
|
||||
const platform = window.navigator.platform;
|
||||
const macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'];
|
||||
const windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE'];
|
||||
const iosPlatforms = ['iPhone', 'iPad', 'iPod'];
|
||||
if (macosPlatforms.indexOf(platform) !== -1) {
|
||||
return Platform.Mac;
|
||||
} else if (iosPlatforms.indexOf(platform) !== -1) {
|
||||
return Platform.Ios;
|
||||
} else if (windowsPlatforms.indexOf(platform) !== -1) {
|
||||
return Platform.Windows;
|
||||
} else if (/Android/.test(userAgent)) {
|
||||
return Platform.Android;
|
||||
} else if (/Linux/.test(platform)) {
|
||||
return Platform.Linux;
|
||||
}
|
||||
return Platform.Unknown;
|
||||
}
|
@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
@ -0,0 +1,19 @@
|
||||
import { store } from 'react-easy-state';
|
||||
|
||||
export interface IDevice {
|
||||
name: string;
|
||||
publicKey: string;
|
||||
endpoint: string;
|
||||
address: string;
|
||||
dns: string;
|
||||
createdAt: string;
|
||||
serverPublicKey: string;
|
||||
// TODO: these fields on backend
|
||||
// receiveBytes: number;
|
||||
// transmitBytes: number;
|
||||
// lastHandshakeTime: string;
|
||||
}
|
||||
|
||||
export const AppState = store({
|
||||
devices: new Array<IDevice>(),
|
||||
});
|
@ -0,0 +1,124 @@
|
||||
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 AddIcon from '@material-ui/icons/Add';
|
||||
import CardActions from '@material-ui/core/CardActions';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
import qrcode from 'qrcode';
|
||||
import { view } from 'react-easy-state';
|
||||
import { box_keyPair } from 'tweetnacl-ts';
|
||||
import { codeBlock } from 'common-tags';
|
||||
import { FormHelperText } from '@material-ui/core';
|
||||
import { GetConnected } from './GetConnected';
|
||||
import { IDevice, AppState } from '../Store';
|
||||
|
||||
class AddDevice extends React.Component {
|
||||
|
||||
state = {
|
||||
name: '',
|
||||
open: false,
|
||||
qrCodeUri: '',
|
||||
configFileUri: '',
|
||||
error: '',
|
||||
};
|
||||
|
||||
onAdd = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
const keypair = box_keyPair();
|
||||
const b64PublicKey = window.btoa(String.fromCharCode(...new Uint8Array(keypair.publicKey) as any));
|
||||
const b64PrivateKey = window.btoa(String.fromCharCode(...new Uint8Array(keypair.secretKey) as any));
|
||||
|
||||
const res = await fetch('/api/devices', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name: this.state.name,
|
||||
publicKey: b64PublicKey,
|
||||
}),
|
||||
});
|
||||
if (res.status >= 400) {
|
||||
this.setState({ error: await res.text() });
|
||||
return;
|
||||
}
|
||||
const { device } = await res.json() as { device: IDevice };
|
||||
|
||||
AppState.devices.push(device);
|
||||
|
||||
const configFile = codeBlock`
|
||||
[Interface]
|
||||
PrivateKey = ${b64PrivateKey}
|
||||
Address = ${device.address}
|
||||
DNS = ${'1.1.1.1, 8.8.8.8'}
|
||||
|
||||
[Peer]
|
||||
PublicKey = ${device.serverPublicKey}
|
||||
AllowedIPs = 0.0.0.0/0
|
||||
Endpoint = ${device.endpoint}
|
||||
`;
|
||||
|
||||
this.setState({
|
||||
open: true,
|
||||
qrCodeUri: await qrcode.toDataURL(configFile),
|
||||
configFileUri: URL.createObjectURL(new Blob([configFile])),
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<form onSubmit={this.onAdd}>
|
||||
<Card>
|
||||
<CardHeader
|
||||
title="Add a device"
|
||||
/>
|
||||
<CardContent>
|
||||
<TextField
|
||||
label="Device Name"
|
||||
error={this.state.error !== ''}
|
||||
value={this.state.name}
|
||||
onChange={(event) => this.setState({ name: event.currentTarget.value })}
|
||||
style={{ marginTop: -20, marginBottom: 8 }}
|
||||
fullWidth
|
||||
/>
|
||||
{this.state.error !== '' && <FormHelperText>{this.state.error}</FormHelperText>}
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<Button
|
||||
color="primary"
|
||||
variant="contained"
|
||||
endIcon={<AddIcon />}
|
||||
type="submit"
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
<Dialog
|
||||
disableBackdropClick
|
||||
disableEscapeKeyDown
|
||||
maxWidth="xl"
|
||||
open={this.state.open}
|
||||
>
|
||||
<DialogTitle>Get Connected</DialogTitle>
|
||||
<DialogContent>
|
||||
<GetConnected
|
||||
qrCodeUri={this.state.qrCodeUri}
|
||||
configFileUri={this.state.configFileUri}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button color="secondary" variant="outlined" onClick={() => this.setState({ open: false })}>
|
||||
Done
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default view(AddDevice);
|
@ -0,0 +1,65 @@
|
||||
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 MenuItem from '@material-ui/core/MenuItem';
|
||||
import formatDistanceToNow from 'date-fns/formatDistanceToNow'
|
||||
import { view } from 'react-easy-state';
|
||||
import { IDevice, AppState } from '../Store';
|
||||
import { IconMenu } from './IconMenu';
|
||||
import { PopoverDisplay } from './PopoverDisplay';
|
||||
|
||||
interface Props {
|
||||
device: IDevice;
|
||||
}
|
||||
|
||||
class Device extends React.Component<Props> {
|
||||
|
||||
dateString(date: Date) {
|
||||
if (date.getUTCMilliseconds() === 0) {
|
||||
return 'never';
|
||||
}
|
||||
return formatDistanceToNow(date, { addSuffix: true });
|
||||
}
|
||||
|
||||
removeDevice = async () => {
|
||||
const res = await fetch(`/api/devices/${this.props.device.name}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (res.status === 204) {
|
||||
AppState.devices = AppState.devices.filter(device => device.name !== this.props.device.name);
|
||||
} else {
|
||||
window.alert(await res.text());
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const device = this.props.device;
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader
|
||||
title={device.name}
|
||||
avatar={<Avatar><DonutSmallIcon /></Avatar>}
|
||||
action={
|
||||
<IconMenu>
|
||||
<MenuItem style={{ color: 'red' }} onClick={this.removeDevice}>Delete</MenuItem>
|
||||
</IconMenu>
|
||||
}
|
||||
/>
|
||||
<CardContent>
|
||||
<Typography component="p">
|
||||
Public Key: <PopoverDisplay label="show">{device.publicKey}</PopoverDisplay>
|
||||
</Typography>
|
||||
<Typography component="p">
|
||||
Endpoint: {device.endpoint}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default view(Device);
|
@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import { view } from 'react-easy-state';
|
||||
import { AppState } from '../Store';
|
||||
import Device from './Device';
|
||||
import AddDevice from './AddDevice';
|
||||
|
||||
class Devices extends React.Component {
|
||||
|
||||
componentDidMount() {
|
||||
this.load();
|
||||
}
|
||||
|
||||
async load() {
|
||||
const res = await fetch('/api/devices');
|
||||
const data = await res.json();
|
||||
AppState.devices = data.items;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Grid container spacing={3} style={{ padding: '1rem' }}>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<h1>Your Devices</h1>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<AddDevice />
|
||||
</Grid>
|
||||
{AppState.devices.map((device, i) =>
|
||||
<Grid key={i} item xs={12} sm={6}>
|
||||
<Device device={device} />
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default view(Devices);
|
@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import Fab from '@material-ui/core/Fab';
|
||||
import VerifiedUserIcon from '@material-ui/icons/VerifiedUser';
|
||||
|
||||
interface Props {
|
||||
configFileUri: string;
|
||||
}
|
||||
|
||||
export class DownloadConfig extends React.Component<Props> {
|
||||
|
||||
downloadConfig = () => {
|
||||
console.log('downloading config file', this.props.configFileUri);
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = this.props.configFileUri;
|
||||
anchor.download = 'wireguard.conf';
|
||||
anchor.style.display = 'none';
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
document.body.removeChild(anchor);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Fab
|
||||
variant="extended"
|
||||
size="small"
|
||||
color="primary"
|
||||
style={{ padding: 30, borderRadius: 60 }}
|
||||
onClick={this.downloadConfig}
|
||||
>
|
||||
Download VPN Config
|
||||
<VerifiedUserIcon style={{ marginLeft: 15 }} />
|
||||
</Fab>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import Fab from '@material-ui/core/Fab';
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
href: string;
|
||||
}
|
||||
|
||||
export class DownloadLink extends React.Component<Props> {
|
||||
render() {
|
||||
return (
|
||||
<a
|
||||
href={this.props.href}
|
||||
target="__blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Fab
|
||||
variant="extended"
|
||||
size="small"
|
||||
color="primary"
|
||||
style={{ padding: 30, borderRadius: 60 }}
|
||||
>
|
||||
<span>{this.props.label}</span>
|
||||
<span style={{ marginLeft: 15 }}>{this.props.icon}</span>
|
||||
</Fab>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,163 @@
|
||||
import React from 'react';
|
||||
import Tabs from '@material-ui/core/Tabs';
|
||||
import Tab from '@material-ui/core/Tab';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import List from '@material-ui/core/List';
|
||||
import ListItem from '@material-ui/core/ListItem';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { MacOSIcon, IosIcon, WindowsIcon, LinuxIcon, AndroidIcon } from './Icons';
|
||||
import { TabPanel } from './TabPanel';
|
||||
import { Platform, getPlatform } from '../Platform';
|
||||
import { DownloadConfig } from './DownloadConfig';
|
||||
import { DownloadLink } from './DownloadLink';
|
||||
|
||||
interface Props {
|
||||
qrCodeUri: string;
|
||||
configFileUri: string;
|
||||
}
|
||||
|
||||
export class GetConnected extends React.Component<Props> {
|
||||
|
||||
state = {
|
||||
platform: getPlatform(),
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Paper>
|
||||
<Tabs
|
||||
value={this.state.platform}
|
||||
onChange={(_, platform) => this.setState({ platform })}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
variant="scrollable"
|
||||
scrollButtons="auto"
|
||||
>
|
||||
<Tab icon={<LinuxIcon />} value={Platform.Linux} />
|
||||
<Tab icon={<MacOSIcon />} value={Platform.Mac} />
|
||||
<Tab icon={<WindowsIcon />} value={Platform.Windows} />
|
||||
<Tab icon={<IosIcon />} value={Platform.Ios} />
|
||||
<Tab icon={<AndroidIcon />} value={Platform.Android} />
|
||||
</Tabs>
|
||||
</Paper>
|
||||
<TabPanel for={Platform.Linux} value={this.state.platform}>
|
||||
<Grid container direction="row" justify="space-around" alignItems="center">
|
||||
<Grid item xs={12} sm={6}>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemText primary="1. hmmm todo" />
|
||||
</ListItem>
|
||||
</List>
|
||||
</Grid>
|
||||
<Grid item direction="column" container spacing={3} xs={12} sm={6}>
|
||||
<Grid item>
|
||||
<DownloadConfig configFileUri={this.props.configFileUri} />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<DownloadLink
|
||||
label="Download WireGuard"
|
||||
href="https://www.wireguard.com/install/"
|
||||
icon={<LinuxIcon />}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</TabPanel>
|
||||
<TabPanel for={Platform.Mac} value={this.state.platform}>
|
||||
<Grid container direction="row" justify="space-around" alignItems="center">
|
||||
<Grid item xs={12} sm={6}>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemText primary="1. Install WireGuard for MacOS" />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText primary="2. Download your connection file" />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText primary="3. Add tunnel from file" />
|
||||
</ListItem>
|
||||
</List>
|
||||
</Grid>
|
||||
<Grid item direction="column" container spacing={3} xs={12} sm={6}>
|
||||
<Grid item>
|
||||
<DownloadConfig configFileUri={this.props.configFileUri} />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<DownloadLink
|
||||
label="Download WireGuard"
|
||||
href="https://itunes.apple.com/us/app/wireguard/id1451685025?ls=1&mt=12"
|
||||
icon={<MacOSIcon />}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</TabPanel>
|
||||
<TabPanel for={Platform.Ios} value={this.state.platform}>
|
||||
<Grid container direction="row" justify="space-around" alignItems="center">
|
||||
<Grid item>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemText primary="1. Install the WireGuard app" />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText primary="2. Add a tunnel" />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText primary="3. Create from QR code" />
|
||||
</ListItem>
|
||||
</List>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<img alt="wireguard qr code" src={this.props.qrCodeUri} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</TabPanel>
|
||||
<TabPanel for={Platform.Android} value={this.state.platform}>
|
||||
<p>TODO: I don't have an android phone :(</p>
|
||||
<p>PRs welcome :)</p>
|
||||
</TabPanel>
|
||||
<TabPanel for={Platform.Windows} value={this.state.platform}>
|
||||
<Grid container direction="row" justify="space-around" alignItems="center">
|
||||
<Grid item xs={12} sm={6}>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemText primary="1. Install WireGuard for Windows" />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText primary="2. Download your connection file" />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText primary="3. Add tunnel from file" />
|
||||
</ListItem>
|
||||
</List>
|
||||
</Grid>
|
||||
<Grid item direction="column" container spacing={3} xs={12} sm={6}>
|
||||
<Grid item>
|
||||
<DownloadConfig configFileUri={this.props.configFileUri} />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<DownloadLink
|
||||
label="Download WireGuard"
|
||||
href="https://download.wireguard.com/windows-client/wireguard-amd64-0.0.32.msi"
|
||||
icon={<WindowsIcon />}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</TabPanel>
|
||||
<Grid container justify="center">
|
||||
<Typography style={{ fontStyle: 'italic', maxWidth: 600 }}>
|
||||
The VPN configuration file or QR code will not be available again.
|
||||
<br />
|
||||
If you lose your connection settings or reset your device, you can remove and re-add it to generate a new connection file or QR code.
|
||||
<br />
|
||||
They contain your WireGuard Private Key and should never be shared.
|
||||
</Typography>
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import MoreVertIcon from '@material-ui/icons/MoreVert';
|
||||
import Menu from '@material-ui/core/Menu';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function IconMenu(props: Props) {
|
||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<IconButton aria-controls="icon-menu" aria-haspopup="true" onClick={handleClick}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
<Menu
|
||||
id="icon-menu"
|
||||
anchorEl={anchorEl}
|
||||
keepMounted
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<div onClick={handleClose}>{props.children}</div>
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
}
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Popover from '@material-ui/core/Popover';
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export class PopoverDisplay extends React.Component<Props> {
|
||||
|
||||
state = {
|
||||
anchorEl: undefined as any,
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Button variant="text" color="secondary" onClick={(event) => this.setState({ anchorEl: event.currentTarget })}>
|
||||
{this.props.label}
|
||||
</Button>
|
||||
<Popover
|
||||
open={Boolean(this.state.anchorEl)}
|
||||
anchorEl={this.state.anchorEl}
|
||||
onClose={() => this.setState({ anchorEl: undefined })}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'center' }}
|
||||
>
|
||||
<div style={{ padding: '2rem' }}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</Popover>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
for: any;
|
||||
value: any;
|
||||
}
|
||||
|
||||
export class TabPanel extends React.Component<Props> {
|
||||
render() {
|
||||
return (
|
||||
<div style={{ padding: '1.5rem 1rem' }} hidden={this.props.for !== this.props.value}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Devices from './components/Devices';
|
||||
import { view } from 'react-easy-state';
|
||||
import 'typeface-roboto';
|
||||
import './index.css';
|
||||
|
||||
const App = view(() => {
|
||||
return <Devices />;
|
||||
});
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue