V2 alpha - initial version (#172)

Initial alpha codebase for version 2 of WireGuard Portal.
This version is considered unstable and incomplete (for example, no public REST API)! 
Use with care!


Fixes/Implements the following issues:
 - OAuth support #154, #1 
 - New Web UI with internationalisation support #98, #107, #89, #62
 - Postgres Support #49 
 - Improved Email handling #47, #119 
 - DNS Search Domain support #46 
 - Bugfixes #94, #48 

---------

Co-authored-by: Fabian Wechselberger <wechselbergerf@hotmail.com>
This commit is contained in:
h44z
2023-08-04 13:34:18 +02:00
committed by GitHub
parent b3a5f2ac60
commit 8b820a5adf
788 changed files with 46139 additions and 11281 deletions

View File

@@ -0,0 +1,200 @@
package configfile
import (
"bufio"
"bytes"
"context"
"fmt"
"github.com/h44z/wg-portal/internal/app"
"github.com/h44z/wg-portal/internal/config"
"github.com/h44z/wg-portal/internal/domain"
"github.com/sirupsen/logrus"
evbus "github.com/vardius/message-bus"
"github.com/yeqown/go-qrcode/v2"
"io"
"os"
"strings"
)
type Manager struct {
cfg *config.Config
bus evbus.MessageBus
tplHandler *TemplateHandler
fsRepo FileSystemRepo // can be nil if storing the configuration is disabled
users UserDatabaseRepo
wg WireguardDatabaseRepo
}
func NewConfigFileManager(cfg *config.Config, bus evbus.MessageBus, users UserDatabaseRepo, wg WireguardDatabaseRepo, fsRepo FileSystemRepo) (*Manager, error) {
tplHandler, err := newTemplateHandler()
if err != nil {
return nil, fmt.Errorf("failed to initialize template handler: %w", err)
}
m := &Manager{
cfg: cfg,
bus: bus,
tplHandler: tplHandler,
fsRepo: fsRepo,
users: users,
wg: wg,
}
if err := m.createStorageDirectory(); err != nil {
return nil, err
}
return m, nil
}
func (m Manager) createStorageDirectory() error {
if m.cfg.Advanced.ConfigStoragePath == "" {
return nil // no storage path configured, skip initialization step
}
err := os.MkdirAll(m.cfg.Advanced.ConfigStoragePath, os.ModePerm)
if err != nil {
return fmt.Errorf("failed to create configuration storage path %s: %w",
m.cfg.Advanced.ConfigStoragePath, err)
}
return nil
}
func (m Manager) connectToMessageBus() {
if m.fsRepo == nil {
return // skip subscription
}
_ = m.bus.Subscribe(app.TopicInterfaceUpdated, m.handleInterfaceUpdatedEvent)
_ = m.bus.Subscribe(app.TopicPeerInterfaceUpdated, m.handleInterfaceUpdatedEvent)
}
func (m Manager) handleInterfaceUpdatedEvent(iface *domain.Interface) {
logrus.Errorf("handling interface updated event for %s", iface.Identifier)
if !iface.SaveConfig || m.fsRepo == nil {
return
}
err := m.PersistInterfaceConfig(context.Background(), iface.Identifier)
if err != nil {
logrus.Errorf("failed to automatically persist interface config for %s: %v", iface.Identifier, err)
}
}
func (m Manager) handlePeerInterfaceUpdatedEvent(id domain.InterfaceIdentifier) {
logrus.Errorf("handling interface updated event for %s", id)
if m.fsRepo == nil {
return
}
peerInterface, err := m.wg.GetInterface(context.Background(), id)
if err != nil {
logrus.Errorf("failed to load interface %s: %v", id, err)
return
}
if !peerInterface.SaveConfig {
return
}
err = m.PersistInterfaceConfig(context.Background(), peerInterface.Identifier)
if err != nil {
logrus.Errorf("failed to automatically persist interface config for %s: %v", peerInterface.Identifier, err)
}
}
func (m Manager) GetInterfaceConfig(ctx context.Context, id domain.InterfaceIdentifier) (io.Reader, error) {
iface, peers, err := m.wg.GetInterfaceAndPeers(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to fetch interface %s: %w", id, err)
}
return m.tplHandler.GetInterfaceConfig(iface, peers)
}
func (m Manager) GetPeerConfig(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error) {
peer, err := m.wg.GetPeer(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to fetch peer %s: %w", id, err)
}
return m.tplHandler.GetPeerConfig(peer)
}
func (m Manager) GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error) {
peer, err := m.wg.GetPeer(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to fetch peer %s: %w", id, err)
}
cfgData, err := m.tplHandler.GetPeerConfig(peer)
if err != nil {
return nil, fmt.Errorf("failed to get peer config for %s: %w", id, err)
}
// remove comments from qr-code config as it is not needed
sb := strings.Builder{}
scanner := bufio.NewScanner(cfgData)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if !strings.HasPrefix(line, "#") {
sb.WriteString(line)
sb.WriteString("\n")
}
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("failed to read peer config for %s: %w", id, err)
}
code, err := qrcode.New(sb.String())
if err != nil {
return nil, fmt.Errorf("failed to initializeqr code for %s: %w", id, err)
}
buf := bytes.NewBuffer(nil)
wr := nopCloser{Writer: buf}
option := Option{
Padding: 8, // padding pixels around the qr code.
BlockSize: 4, // block pixels which represents a bit data.
}
qrWriter := NewCompressedWriter(wr, &option)
err = code.Save(qrWriter)
if err != nil {
return nil, fmt.Errorf("failed to write code for %s: %w", id, err)
}
return buf, nil
}
func (m Manager) PersistInterfaceConfig(ctx context.Context, id domain.InterfaceIdentifier) error {
if m.fsRepo == nil {
return fmt.Errorf("peristing configuration is not supported")
}
iface, peers, err := m.wg.GetInterfaceAndPeers(ctx, id)
if err != nil {
return fmt.Errorf("failed to fetch interface %s: %w", id, err)
}
cfg, err := m.tplHandler.GetInterfaceConfig(iface, peers)
if err != nil {
return fmt.Errorf("failed to get interface config: %w", err)
}
if err := m.fsRepo.WriteFile(iface.GetConfigFileName(), cfg); err != nil {
return fmt.Errorf("failed to write interface config: %w", err)
}
return nil
}
type nopCloser struct {
io.Writer
}
func (nopCloser) Close() error { return nil }

View File

@@ -0,0 +1,88 @@
package configfile
// waiting for https://github.com/yeqown/go-qrcode/pull/85 to get merged
// meanwhile we use our own writer implementation
import (
"image"
"image/color"
"image/png"
"io"
"github.com/yeqown/go-qrcode/v2"
)
type Option struct {
Padding int
BlockSize int
}
// compressedWriter implements issue#69, generating compressed images
// in some special situations, such as, network transferring.
// https://github.com/yeqown/go-qrcode/issues/69
type compressedWriter struct {
fd io.WriteCloser
option *Option
}
var (
backgroundColor = color.Gray{Y: 0xff}
foregroundColor = color.Gray{Y: 0x00}
)
func NewCompressedWriter(writer io.WriteCloser, opt *Option) qrcode.Writer {
return compressedWriter{fd: writer, option: opt}
}
func (w compressedWriter) Write(mat qrcode.Matrix) error {
padding := w.option.Padding
blockWidth := w.option.BlockSize
width := mat.Width()*blockWidth + 2*padding
height := width
img := image.NewPaletted(
image.Rect(0, 0, width, height),
color.Palette([]color.Color{backgroundColor, foregroundColor}),
)
bgColor := uint8(img.Palette.Index(backgroundColor))
fgColor := uint8(img.Palette.Index(foregroundColor))
rectangle := func(x1, y1 int, x2, y2 int, img *image.Paletted, color uint8) {
for x := x1; x < x2; x++ {
for y := y1; y < y2; y++ {
pos := img.PixOffset(x, y)
img.Pix[pos] = color
}
}
}
// background
rectangle(0, 0, width, height, img, bgColor)
mat.Iterate(qrcode.IterDirection_COLUMN, func(x int, y int, v qrcode.QRValue) {
sx := x*blockWidth + padding
sy := y*blockWidth + padding
es := (x+1)*blockWidth + padding
ey := (y+1)*blockWidth + padding
if v.IsSet() {
rectangle(sx, sy, es, ey, img, fgColor)
}
//switch v.IsSet() {
//case false:
// gray = backgroundColor
//default:
// gray = foregroundColor
//}
})
encoder := png.Encoder{CompressionLevel: png.BestCompression}
return encoder.Encode(w.fd, img)
}
func (w compressedWriter) Close() error {
return w.fd.Close()
}

View File

@@ -0,0 +1,21 @@
package configfile
import (
"context"
"github.com/h44z/wg-portal/internal/domain"
"io"
)
type UserDatabaseRepo interface {
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
}
type WireguardDatabaseRepo interface {
GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error)
GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error)
GetInterface(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, error)
}
type FileSystemRepo interface {
WriteFile(path string, contents io.Reader) error
}

View File

@@ -0,0 +1,68 @@
package configfile
import (
"bytes"
"embed"
"fmt"
"io"
"text/template"
"github.com/h44z/wg-portal/internal/domain"
)
//go:embed tpl_files/*
var TemplateFiles embed.FS
type TemplateHandler struct {
templates *template.Template
}
func newTemplateHandler() (*TemplateHandler, error) {
tplFuncs := template.FuncMap{
"CidrsToString": domain.CidrsToString,
}
templateCache, err := template.New("WireGuard").Funcs(tplFuncs).ParseFS(TemplateFiles, "tpl_files/*.tpl")
if err != nil {
return nil, err
}
handler := &TemplateHandler{
templates: templateCache,
}
return handler, nil
}
func (c TemplateHandler) GetInterfaceConfig(cfg *domain.Interface, peers []domain.Peer) (io.Reader, error) {
var tplBuff bytes.Buffer
err := c.templates.ExecuteTemplate(&tplBuff, "wg_interface.tpl", map[string]interface{}{
"Interface": cfg,
"Peers": peers,
"Portal": map[string]interface{}{
"Version": "unknown",
},
})
if err != nil {
return nil, fmt.Errorf("failed to execute interface template for %s: %w", cfg.Identifier, err)
}
return &tplBuff, nil
}
func (c TemplateHandler) GetPeerConfig(peer *domain.Peer) (io.Reader, error) {
var tplBuff bytes.Buffer
err := c.templates.ExecuteTemplate(&tplBuff, "wg_peer.tpl", map[string]interface{}{
"Peer": peer,
"Portal": map[string]interface{}{
"Version": "unknown",
},
})
if err != nil {
return nil, fmt.Errorf("failed to execute peer template for %s: %w", peer.Identifier, err)
}
return &tplBuff, nil
}

View File

@@ -0,0 +1,89 @@
# AUTOGENERATED FILE - DO NOT EDIT
# This file uses wg-quick format.
# See https://man7.org/linux/man-pages/man8/wg-quick.8.html#CONFIGURATION
# Lines starting with the -WGP- tag are used by
# the WireGuard Portal configuration parser.
# -WGP- WIREGUARD PORTAL CONFIGURATION FILE
# -WGP- version {{ .Portal.Version }}
[Interface]
# -WGP- Interface: {{ .Interface.Identifier }}
# -WGP- Created: {{ .Interface.CreatedAt }}
# -WGP- Updated: {{ .Interface.UpdatedAt }}
# -WGP- Display name: {{ .Interface.DisplayName }}
# -WGP- Interface mode: {{ .Interface.Type }}
# -WGP- PublicKey = {{ .Interface.KeyPair.PublicKey }}
# Core settings
PrivateKey = {{ .Interface.KeyPair.PrivateKey }}
Address = {{ CidrsToString .Interface.Addresses }}
# Misc. settings (optional)
{{- if ne .Interface.ListenPort 0}}
ListenPort = {{ .Interface.ListenPort }}
{{- end}}
{{- if ne .Interface.Mtu 0}}
MTU = {{.Interface.Mtu}}
{{- end}}
{{- if and (ne .Interface.DnsStr "") (eq $.Interface.Type "client")}}
DNS = {{ .Interface.DnsStr }}
{{- end}}
{{- if ne .Interface.FirewallMark 0}}
FwMark = {{.Interface.FirewallMark}}
{{- end}}
{{- if ne .Interface.RoutingTable ""}}
Table = {{.Interface.RoutingTable}}
{{- end}}
{{- if .Interface.SaveConfig}}
SaveConfig = true
{{- end}}
# Interface hooks (optional)
{{- if .Interface.PreUp}}
PreUp = {{ .Interface.PreUp }}
{{- end}}
{{- if .Interface.PostUp}}
PostUp = {{ .Interface.PostUp }}
{{- end}}
{{- if .Interface.PreDown}}
PreDown = {{ .Interface.PreDown }}
{{- end}}
{{- if .Interface.PostDown}}
PostDown = {{ .Interface.PostDown }}
{{- end}}
#
# Peers
#
{{range .Peers}}
{{- if not .IsDisabled}}
[Peer]
# -WGP- Peer: {{.Identifier}}
# -WGP- Created: {{.CreatedAt}}
# -WGP- Updated: {{.UpdatedAt}}
# -WGP- Display name: {{ .DisplayName }}
{{- if .Interface.KeyPair.PrivateKey}}
# -WGP- PrivateKey: {{.Interface.KeyPair.PrivateKey}}
{{- end}}
PublicKey = {{ .Interface.KeyPair.PublicKey }}
{{- if .PresharedKey}}
PresharedKey = {{ .PresharedKey }}
{{- end}}
{{- if eq $.Interface.Type "server"}}
AllowedIPs = {{ CidrsToString .Interface.Addresses }}{{if ne .ExtraAllowedIPsStr ""}}, {{ .ExtraAllowedIPsStr }}{{end}}
{{- end}}
{{- if eq $.Interface.Type "client"}}
{{- if .AllowedIPsStr.GetValue}}
AllowedIPs = {{ .AllowedIPsStr.GetValue }}
{{- end}}
{{- end}}
{{- if and (ne .Endpoint.GetValue "") (eq $.Interface.Type "client")}}
Endpoint = {{ .Endpoint.GetValue }}
{{- end}}
{{- if and (ne .PersistentKeepalive.GetValue 0) (eq $.Interface.Type "client")}}
PersistentKeepalive = {{ .PersistentKeepalive.GetValue }}
{{- end}}
{{- end}}
{{end}}

View File

@@ -0,0 +1,65 @@
# AUTOGENERATED FILE - DO NOT EDIT
# This file uses wg-quick format.
# See https://man7.org/linux/man-pages/man8/wg-quick.8.html#CONFIGURATION
# Lines starting with the -WGP- tag are used by
# the WireGuard Portal configuration parser.
# -WGP- WIREGUARD PORTAL CONFIGURATION FILE
# -WGP- version {{ .Portal.Version }}
[Interface]
# -WGP- Peer: {{.Peer.Identifier}}
# -WGP- Created: {{.Peer.CreatedAt}}
# -WGP- Updated: {{.Peer.UpdatedAt}}
# -WGP- Display name: {{ .Peer.DisplayName }}
# -WGP- PublicKey: {{ .Peer.Interface.KeyPair.PublicKey }}
{{- if eq .Peer.Interface.Type "server"}}
# -WGP- Peer type: server
{{else}}
# -WGP- Peer type: client
{{- end}}
# Core settings
PrivateKey = {{ .Peer.Interface.KeyPair.PrivateKey }}
Address = {{ CidrsToString .Peer.Interface.Addresses }}
# Misc. settings (optional)
{{- if .Peer.Interface.DnsStr.GetValue}}
DNS = {{ .Peer.Interface.DnsStr.GetValue }}
{{- end}}
{{- if ne .Peer.Interface.Mtu.GetValue 0}}
MTU = {{ .Peer.Interface.Mtu.GetValue }}
{{- end}}
{{- if ne .Peer.Interface.FirewallMark.GetValue 0}}
FwMark = {{ .Peer.Interface.FirewallMark.GetValue }}
{{- end}}
{{- if ne .Peer.Interface.RoutingTable.GetValue ""}}
Table = {{ .Peer.Interface.RoutingTable.GetValue }}
{{- end}}
# Interface hooks (optional)
{{- if .Peer.Interface.PreUp.GetValue}}
PreUp = {{ .Peer.Interface.PreUp.GetValue }}
{{- end}}
{{- if .Peer.Interface.PostUp.GetValue}}
PostUp = {{ .Peer.Interface.PostUp.GetValue }}
{{- end}}
{{- if .Peer.Interface.PreDown.GetValue}}
PreDown = {{ .Peer.Interface.PreDown.GetValue }}
{{- end}}
{{- if .Peer.Interface.PostDown.GetValue}}
PostDown = {{ .Peer.Interface.PostDown.GetValue }}
{{- end}}
[Peer]
PublicKey = {{ .Peer.EndpointPublicKey.GetValue }}
Endpoint = {{ .Peer.Endpoint.GetValue }}
{{- if .Peer.AllowedIPsStr.GetValue}}
AllowedIPs = {{ .Peer.AllowedIPsStr.GetValue }}
{{- end}}
{{- if .Peer.PresharedKey}}
PresharedKey = {{ .Peer.PresharedKey }}
{{- end}}
{{- if and (ne .Peer.PersistentKeepalive.GetValue 0) (eq .Peer.Interface.Type "client")}}
PersistentKeepalive = {{ .Peer.PersistentKeepalive.GetValue }}
{{- end}}