mirror of
https://github.com/h44z/wg-portal.git
synced 2025-09-13 06:21:15 +00:00
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:
200
internal/app/configfile/manager.go
Normal file
200
internal/app/configfile/manager.go
Normal 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 }
|
88
internal/app/configfile/qrwriter.go
Normal file
88
internal/app/configfile/qrwriter.go
Normal 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()
|
||||
}
|
21
internal/app/configfile/repos.go
Normal file
21
internal/app/configfile/repos.go
Normal 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
|
||||
}
|
68
internal/app/configfile/template.go
Normal file
68
internal/app/configfile/template.go
Normal 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
|
||||
}
|
89
internal/app/configfile/tpl_files/wg_interface.tpl
Normal file
89
internal/app/configfile/tpl_files/wg_interface.tpl
Normal 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}}
|
65
internal/app/configfile/tpl_files/wg_peer.tpl
Normal file
65
internal/app/configfile/tpl_files/wg_peer.tpl
Normal 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}}
|
Reference in New Issue
Block a user