wip: create/update/...

This commit is contained in:
Christoph Haas 2020-11-07 18:36:23 +01:00
parent ea65e6b43c
commit cc06019738
8 changed files with 164 additions and 195 deletions

View File

@ -34,10 +34,12 @@
<form method="post" enctype="multipart/form-data"> <form method="post" enctype="multipart/form-data">
<input type="hidden" name="uid" value="{{.Peer.UID}}"> <input type="hidden" name="uid" value="{{.Peer.UID}}">
<input type="hidden" name="privkey" value="{{.Peer.PrivateKey}}">
<input type="hidden" name="presharedkey" value="{{.Peer.PresharedKey}}">
<div class="form-row"> <div class="form-row">
<div class="form-group col-md-12"> <div class="form-group col-md-12">
<label for="inputServerPublicKey">Public Key</label> <label for="inputServerPublicKey">Public Key</label>
<input type="text" name="pkey" disabled class="form-control" id="inputServerPublicKey" value="{{.Peer.PublicKey}}"> <input type="text" name="pubkey" readonly class="form-control" id="inputServerPublicKey" value="{{.Peer.PublicKey}}">
</div> </div>
</div> </div>
<div class="form-row"> <div class="form-row">

View File

@ -30,11 +30,12 @@
<form method="post" enctype="multipart/form-data"> <form method="post" enctype="multipart/form-data">
<input type="hidden" name="device" value="{{.Device.DeviceName}}"> <input type="hidden" name="device" value="{{.Device.DeviceName}}">
<input type="hidden" name="privkey" value="{{.Device.PrivateKey}}">
<h3>Server's interface configuration</h3> <h3>Server's interface configuration</h3>
<div class="form-row"> <div class="form-row">
<div class="form-group col-md-12"> <div class="form-group col-md-12">
<label for="inputServerPublicKey">Public Key</label> <label for="inputServerPublicKey">Public Key</label>
<input type="text" name="pubkey" disabled class="form-control" id="inputServerPublicKey" value="{{.Device.PublicKey}}"> <input type="text" name="pubkey" readonly class="form-control" id="inputServerPublicKey" value="{{.Device.PublicKey}}">
</div> </div>
</div> </div>
<div class="form-row"> <div class="form-row">

View File

@ -14,7 +14,7 @@
<body id="page-top"> <body id="page-top">
{{template "prt_nav.html" .}} {{template "prt_nav.html" .}}
<div class="container"> <div class="container">
<h1>WireGuard VPN Administration</h1> <h1 class="mt-2">WireGuard VPN Administration</h1>
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
@ -77,9 +77,16 @@
</div> </div>
</div> </div>
<div></div> <div class="mt-4 row">
<div class="col-sm-10 col-12">
<h2>Current VPN Users</h2> <h2>Current VPN Users</h2>
<div class="table-responsive"> </div>
<div class="col-sm-2 col-12">
<a href="/admin/peer/create" title="Add a LDAP user" class="btn btn-primary pull-right"><i class="fa fa-fw fa-user-plus"></i></a>
<a href="/admin/peer/create" title="Manual add a user" class="btn btn-primary pull-right"><i class="fa fa-fw fa-plus"></i>M</a>
</div>
</div>
<div class="mt-2 table-responsive">
<table class="table table-sm" id="userTable"> <table class="table table-sm" id="userTable">
<thead> <thead>
<tr> <tr>

1
go.mod
View File

@ -7,6 +7,7 @@ require (
github.com/gin-gonic/contrib v0.0.0-20201005132743-ca038bbf2944 github.com/gin-gonic/contrib v0.0.0-20201005132743-ca038bbf2944
github.com/gin-gonic/gin v1.6.3 github.com/gin-gonic/gin v1.6.3
github.com/go-ldap/ldap/v3 v3.2.4 github.com/go-ldap/ldap/v3 v3.2.4
github.com/go-playground/validator/v10 v10.2.0
github.com/gorilla/sessions v1.2.1 // indirect github.com/gorilla/sessions v1.2.1 // indirect
github.com/kelseyhightower/envconfig v1.4.0 github.com/kelseyhightower/envconfig v1.4.0
github.com/sirupsen/logrus v1.7.0 github.com/sirupsen/logrus v1.7.0

View File

@ -1,6 +1,9 @@
package common package common
import "net" import (
"net"
"strings"
)
// BroadcastAddr returns the last address in the given network, or the broadcast address. // BroadcastAddr returns the last address in the given network, or the broadcast address.
func BroadcastAddr(n *net.IPNet) net.IP { func BroadcastAddr(n *net.IPNet) net.IP {
@ -35,3 +38,20 @@ func IsIPv6(address string) bool {
} }
return ip.To4() == nil return ip.To4() == nil
} }
func ParseIPList(lst string) []string {
ips := strings.Split(lst, ",")
validatedIPs := make([]string, 0, len(ips))
for i := range ips {
ips[i] = strings.TrimSpace(ips[i])
if ips[i] != "" {
validatedIPs = append(validatedIPs, ips[i])
}
}
return validatedIPs
}
func IPListToString(lst []string) string {
return strings.Join(lst, ", ")
}

View File

@ -38,6 +38,7 @@ type SessionData struct {
Search string Search string
AlertData string AlertData string
AlertType string AlertType string
FormData interface{}
} }
type AlertData struct { type AlertData struct {

View File

@ -1,12 +1,15 @@
package server package server
import ( import (
"crypto/md5"
"fmt"
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/h44z/wg-portal/internal/common"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes" "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -77,72 +80,22 @@ func (s *Server) GetAdminEditInterface(c *gin.Context) {
} }
func (s *Server) PostAdminEditInterface(c *gin.Context) { func (s *Server) PostAdminEditInterface(c *gin.Context) {
device := s.users.GetDevice() var formDevice Device
var err error if err := c.ShouldBind(&formDevice); err != nil {
s.setAlert(c, "failed to bind form data: "+err.Error(), "danger")
device.ListenPort, err = strconv.Atoi(c.PostForm("port"))
if err != nil {
s.setAlert(c, "invalid port: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/device/edit")
return
}
ipField := c.PostForm("ip")
ips := strings.Split(ipField, ",")
validatedIPs := make([]string, 0, len(ips))
for i := range ips {
ips[i] = strings.TrimSpace(ips[i])
if ips[i] != "" {
validatedIPs = append(validatedIPs, ips[i])
}
}
if len(validatedIPs) == 0 {
s.setAlert(c, "invalid ip address", "danger")
c.Redirect(http.StatusSeeOther, "/admin/device/edit")
return
}
device.IPs = validatedIPs
device.Endpoint = c.PostForm("endpoint")
dnsField := c.PostForm("dns")
dns := strings.Split(dnsField, ",")
validatedDNS := make([]string, 0, len(dns))
for i := range dns {
dns[i] = strings.TrimSpace(dns[i])
if dns[i] != "" {
validatedDNS = append(validatedDNS, dns[i])
}
}
device.DNS = validatedDNS
allowedIPField := c.PostForm("allowedip")
allowedIP := strings.Split(allowedIPField, ",")
validatedAllowedIP := make([]string, 0, len(allowedIP))
for i := range allowedIP {
allowedIP[i] = strings.TrimSpace(allowedIP[i])
if allowedIP[i] != "" {
validatedAllowedIP = append(validatedAllowedIP, allowedIP[i])
}
}
device.AllowedIPs = validatedAllowedIP
device.Mtu, err = strconv.Atoi(c.PostForm("mtu"))
if err != nil {
s.setAlert(c, "invalid MTU: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/device/edit")
return
}
device.PersistentKeepalive, err = strconv.Atoi(c.PostForm("keepalive"))
if err != nil {
s.setAlert(c, "invalid PersistentKeepalive: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/device/edit") c.Redirect(http.StatusSeeOther, "/admin/device/edit")
return return
} }
// Clean list input
formDevice.IPs = common.ParseIPList(formDevice.IPsStr)
formDevice.AllowedIPs = common.ParseIPList(formDevice.AllowedIPsStr)
formDevice.DNS = common.ParseIPList(formDevice.DNSStr)
formDevice.IPsStr = common.IPListToString(formDevice.IPs)
formDevice.AllowedIPsStr = common.IPListToString(formDevice.AllowedIPs)
formDevice.DNSStr = common.IPListToString(formDevice.DNS)
// Update WireGuard device // Update WireGuard device
err = s.wg.UpdateDevice(device.DeviceName, device.GetDeviceConfig()) err := s.wg.UpdateDevice(formDevice.DeviceName, formDevice.GetDeviceConfig())
if err != nil { if err != nil {
s.setAlert(c, "failed to update device in WireGuard: "+err.Error(), "danger") s.setAlert(c, "failed to update device in WireGuard: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/device/edit") c.Redirect(http.StatusSeeOther, "/admin/device/edit")
@ -150,7 +103,7 @@ func (s *Server) PostAdminEditInterface(c *gin.Context) {
} }
// Update in database // Update in database
err = s.users.UpdateDevice(device) err = s.users.UpdateDevice(formDevice)
if err != nil { if err != nil {
s.setAlert(c, "failed to update device in database: "+err.Error(), "danger") s.setAlert(c, "failed to update device in database: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/device/edit") c.Redirect(http.StatusSeeOther, "/admin/device/edit")
@ -183,77 +136,47 @@ func (s *Server) GetAdminEditPeer(c *gin.Context) {
} }
func (s *Server) PostAdminEditPeer(c *gin.Context) { func (s *Server) PostAdminEditPeer(c *gin.Context) {
user := s.users.GetUserByKey(c.Query("pkey")) currentUser := s.users.GetUserByKey(c.Query("pkey"))
urlEncodedKey := url.QueryEscape(c.Query("pkey")) urlEncodedKey := url.QueryEscape(c.Query("pkey"))
var err error
user.Identifier = c.PostForm("identifier") var formUser User
if user.Identifier == "" { if err := c.ShouldBind(&formUser); err != nil {
s.setAlert(c, "invalid identifier, must not be empty", "danger") s.setAlert(c, "failed to bind form data: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/peer/edit?pkey="+urlEncodedKey) c.Redirect(http.StatusSeeOther, "/admin/peer/edit?pkey="+urlEncodedKey)
return return
} }
user.Email = c.PostForm("mail") // Clean list input
if user.Email == "" { formUser.IPs = common.ParseIPList(formUser.IPsStr)
s.setAlert(c, "invalid email, must not be empty", "danger") formUser.AllowedIPs = common.ParseIPList(formUser.AllowedIPsStr)
c.Redirect(http.StatusSeeOther, "/admin/peer/edit?pkey="+urlEncodedKey) formUser.IPsStr = common.IPListToString(formUser.IPs)
return formUser.AllowedIPsStr = common.IPListToString(formUser.AllowedIPs)
}
ipField := c.PostForm("ip")
ips := strings.Split(ipField, ",")
validatedIPs := make([]string, 0, len(ips))
for i := range ips {
ips[i] = strings.TrimSpace(ips[i])
if ips[i] != "" {
validatedIPs = append(validatedIPs, ips[i])
}
}
if len(validatedIPs) == 0 {
s.setAlert(c, "invalid ip address", "danger")
c.Redirect(http.StatusSeeOther, "/admin/peer/edit?pkey="+urlEncodedKey)
return
}
user.IPs = validatedIPs
allowedIPField := c.PostForm("allowedip")
allowedIP := strings.Split(allowedIPField, ",")
validatedAllowedIP := make([]string, 0, len(allowedIP))
for i := range allowedIP {
allowedIP[i] = strings.TrimSpace(allowedIP[i])
if allowedIP[i] != "" {
validatedAllowedIP = append(validatedAllowedIP, allowedIP[i])
}
}
user.AllowedIPs = validatedAllowedIP
user.IgnorePersistentKeepalive = c.PostForm("ignorekeepalive") != ""
disabled := c.PostForm("isdisabled") != "" disabled := c.PostForm("isdisabled") != ""
now := time.Now() now := time.Now()
if disabled && user.DeactivatedAt == nil { if disabled && currentUser.DeactivatedAt == nil {
user.DeactivatedAt = &now formUser.DeactivatedAt = &now
} else if !disabled { } else if !disabled {
user.DeactivatedAt = nil formUser.DeactivatedAt = nil
} }
// Update WireGuard device // Update WireGuard device
if user.DeactivatedAt == &now { if formUser.DeactivatedAt == &now {
err = s.wg.RemovePeer(user.PublicKey) err := s.wg.RemovePeer(formUser.PublicKey)
if err != nil { if err != nil {
s.setAlert(c, "failed to remove peer in WireGuard: "+err.Error(), "danger") s.setAlert(c, "failed to remove peer in WireGuard: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/peer/edit?pkey="+urlEncodedKey) c.Redirect(http.StatusSeeOther, "/admin/peer/edit?pkey="+urlEncodedKey)
return return
} }
} else if user.DeactivatedAt == nil && user.Peer != nil { } else if formUser.DeactivatedAt == nil && currentUser.Peer != nil {
err = s.wg.UpdatePeer(user.GetPeerConfig()) err := s.wg.UpdatePeer(formUser.GetPeerConfig())
if err != nil { if err != nil {
s.setAlert(c, "failed to update peer in WireGuard: "+err.Error(), "danger") s.setAlert(c, "failed to update peer in WireGuard: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/peer/edit?pkey="+urlEncodedKey) c.Redirect(http.StatusSeeOther, "/admin/peer/edit?pkey="+urlEncodedKey)
return return
} }
} else if user.DeactivatedAt == nil && user.Peer == nil { } else if formUser.DeactivatedAt == nil && currentUser.Peer == nil {
err = s.wg.AddPeer(user.GetPeerConfig()) err := s.wg.AddPeer(formUser.GetPeerConfig())
if err != nil { if err != nil {
s.setAlert(c, "failed to add peer in WireGuard: "+err.Error(), "danger") s.setAlert(c, "failed to add peer in WireGuard: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/peer/edit?pkey="+urlEncodedKey) c.Redirect(http.StatusSeeOther, "/admin/peer/edit?pkey="+urlEncodedKey)
@ -262,7 +185,7 @@ func (s *Server) PostAdminEditPeer(c *gin.Context) {
} }
// Update in database // Update in database
err = s.users.UpdateUser(user) err := s.users.UpdateUser(formUser)
if err != nil { if err != nil {
s.setAlert(c, "failed to update user in database: "+err.Error(), "danger") s.setAlert(c, "failed to update user in database: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/peer/edit?pkey="+urlEncodedKey) c.Redirect(http.StatusSeeOther, "/admin/peer/edit?pkey="+urlEncodedKey)
@ -278,6 +201,20 @@ func (s *Server) GetAdminCreatePeer(c *gin.Context) {
user := User{} user := User{}
user.AllowedIPsStr = device.AllowedIPsStr user.AllowedIPsStr = device.AllowedIPsStr
user.IPsStr = "" // TODO: add a valid ip here user.IPsStr = "" // TODO: add a valid ip here
psk, err := wgtypes.GenerateKey()
if err != nil {
s.HandleError(c, http.StatusInternalServerError, "Preshared key generation error", err.Error())
return
}
key, err := wgtypes.GeneratePrivateKey()
if err != nil {
s.HandleError(c, http.StatusInternalServerError, "Private key generation error", err.Error())
return
}
user.PresharedKey = psk.String()
user.PrivateKey = key.String()
user.PublicKey = key.PublicKey().String()
user.UID = fmt.Sprintf("u%x", md5.Sum([]byte(user.PublicKey)))
c.HTML(http.StatusOK, "admin_edit_client.html", struct { c.HTML(http.StatusOK, "admin_edit_client.html", struct {
Route string Route string
@ -297,68 +234,28 @@ func (s *Server) GetAdminCreatePeer(c *gin.Context) {
} }
func (s *Server) PostAdminCreatePeer(c *gin.Context) { func (s *Server) PostAdminCreatePeer(c *gin.Context) {
user := User{} var formUser User
key, err := wgtypes.GeneratePrivateKey() if err := c.ShouldBind(&formUser); err != nil {
if err != nil { s.setAlert(c, "failed to bind form data: "+err.Error(), "danger")
s.HandleError(c, http.StatusInternalServerError, "Private key generation error", err.Error())
return
}
user.PrivateKey = key.String()
user.PublicKey = key.PublicKey().String()
user.Identifier = c.PostForm("identifier")
if user.Identifier == "" {
s.setAlert(c, "invalid identifier, must not be empty", "danger")
c.Redirect(http.StatusSeeOther, "/admin/peer/create") c.Redirect(http.StatusSeeOther, "/admin/peer/create")
return return
} }
user.Email = c.PostForm("mail") // Clean list input
if user.Email == "" { formUser.IPs = common.ParseIPList(formUser.IPsStr)
s.setAlert(c, "invalid email, must not be empty", "danger") formUser.AllowedIPs = common.ParseIPList(formUser.AllowedIPsStr)
c.Redirect(http.StatusSeeOther, "/admin/peer/create") formUser.IPsStr = common.IPListToString(formUser.IPs)
return formUser.AllowedIPsStr = common.IPListToString(formUser.AllowedIPs)
}
ipField := c.PostForm("ip")
ips := strings.Split(ipField, ",")
validatedIPs := make([]string, 0, len(ips))
for i := range ips {
ips[i] = strings.TrimSpace(ips[i])
if ips[i] != "" {
validatedIPs = append(validatedIPs, ips[i])
}
}
if len(validatedIPs) == 0 {
s.setAlert(c, "invalid ip address", "danger")
c.Redirect(http.StatusSeeOther, "/admin/peer/create")
return
}
user.IPs = validatedIPs
allowedIPField := c.PostForm("allowedip")
allowedIP := strings.Split(allowedIPField, ",")
validatedAllowedIP := make([]string, 0, len(allowedIP))
for i := range allowedIP {
allowedIP[i] = strings.TrimSpace(allowedIP[i])
if allowedIP[i] != "" {
validatedAllowedIP = append(validatedAllowedIP, allowedIP[i])
}
}
user.AllowedIPs = validatedAllowedIP
user.IgnorePersistentKeepalive = c.PostForm("ignorekeepalive") != ""
disabled := c.PostForm("isdisabled") != "" disabled := c.PostForm("isdisabled") != ""
now := time.Now() now := time.Now()
if disabled && user.DeactivatedAt == nil { if disabled {
user.DeactivatedAt = &now formUser.DeactivatedAt = &now
} else if !disabled {
user.DeactivatedAt = nil
} }
// Update WireGuard device // Update WireGuard device
if user.DeactivatedAt == nil { if formUser.DeactivatedAt == nil {
err = s.wg.AddPeer(user.GetPeerConfig()) err := s.wg.AddPeer(formUser.GetPeerConfig())
if err != nil { if err != nil {
s.setAlert(c, "failed to add peer in WireGuard: "+err.Error(), "danger") s.setAlert(c, "failed to add peer in WireGuard: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/peer/create") c.Redirect(http.StatusSeeOther, "/admin/peer/create")
@ -367,7 +264,7 @@ func (s *Server) PostAdminCreatePeer(c *gin.Context) {
} }
// Update in database // Update in database
err = s.users.CreateUser(user) err := s.users.CreateUser(formUser)
if err != nil { if err != nil {
s.setAlert(c, "failed to add user in database: "+err.Error(), "danger") s.setAlert(c, "failed to add user in database: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/peer/create") c.Redirect(http.StatusSeeOther, "/admin/peer/create")

View File

@ -10,6 +10,10 @@ import (
"text/template" "text/template"
"time" "time"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
"github.com/h44z/wg-portal/internal/wireguard" "github.com/h44z/wg-portal/internal/wireguard"
"github.com/h44z/wg-portal/internal/common" "github.com/h44z/wg-portal/internal/common"
@ -22,6 +26,42 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
//
// CUSTOM VALIDATORS ----------------------------------------------------------------------------
//
var cidrList validator.Func = func(fl validator.FieldLevel) bool {
cidrListStr := fl.Field().String()
cidrList := common.ParseIPList(cidrListStr)
for i := range cidrList {
_, _, err := net.ParseCIDR(cidrList[i])
if err != nil {
return false
}
}
return true
}
var ipList validator.Func = func(fl validator.FieldLevel) bool {
ipListStr := fl.Field().String()
ipList := common.ParseIPList(ipListStr)
for i := range ipList {
ip := net.ParseIP(ipList[i])
if ip == nil {
return false
}
}
return true
}
func init() {
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterValidation("cidrlist", cidrList)
v.RegisterValidation("iplist", ipList)
}
}
// //
// USER ---------------------------------------------------------------------------------------- // USER ----------------------------------------------------------------------------------------
// //
@ -31,19 +71,19 @@ type User struct {
LdapUser *ldap.UserCacheHolderEntry `gorm:"-"` // optional, it is still possible to have users without ldap LdapUser *ldap.UserCacheHolderEntry `gorm:"-"` // optional, it is still possible to have users without ldap
Config string `gorm:"-"` Config string `gorm:"-"`
UID string // uid for html identification UID string `form:"uid" binding:"alphanum"` // uid for html identification
IsOnline bool `gorm:"-"` IsOnline bool `gorm:"-"`
Identifier string // Identifier AND Email make a WireGuard peer unique Identifier string `form:"identifier" binding:"required,lt=64"` // Identifier AND Email make a WireGuard peer unique
Email string `gorm:"index"` Email string `gorm:"index" form:"mail" binding:"required,email"`
IgnorePersistentKeepalive bool IgnorePersistentKeepalive bool `form:"ignorekeepalive"`
PresharedKey string PresharedKey string `form:"presharedkey" binding:"omitempty,base64"`
AllowedIPsStr string AllowedIPsStr string `form:"allowedip" binding:"cidrlist"`
IPsStr string IPsStr string `form:"ip" binding:"cidrlist"`
AllowedIPs []string `gorm:"-"` // IPs that are used in the client config file AllowedIPs []string `gorm:"-"` // IPs that are used in the client config file
IPs []string `gorm:"-"` // The IPs of the client IPs []string `gorm:"-"` // The IPs of the client
PrivateKey string PrivateKey string `form:"privkey" binding:"omitempty,base64"`
PublicKey string `gorm:"primaryKey"` PublicKey string `gorm:"primaryKey" form:"pubkey" binding:"required,base64"`
DeactivatedAt *time.Time DeactivatedAt *time.Time
CreatedBy string CreatedBy string
@ -128,23 +168,23 @@ func (u User) IsValid() bool {
type Device struct { type Device struct {
Interface *wgtypes.Device `gorm:"-"` Interface *wgtypes.Device `gorm:"-"`
DeviceName string `gorm:"primaryKey"` DeviceName string `form:"device" gorm:"primaryKey" binding:"required,alphanum"`
PrivateKey string PrivateKey string `form:"privkey" binding:"base64"`
PublicKey string PublicKey string `form:"pubkey" binding:"required,base64"`
PersistentKeepalive int PersistentKeepalive int `form:"keepalive" binding:"gte=0"`
ListenPort int ListenPort int `form:"port" binding:"required,gt=0"`
Mtu int Mtu int `form:"mtu" binding:"gte=0,lte=1500"`
Endpoint string Endpoint string `form:"endpoint" binding:"required,hostname_port"`
AllowedIPsStr string AllowedIPsStr string `form:"allowedip" binding:"cidrlist"`
IPsStr string IPsStr string `form:"ip" binding:"required,cidrlist"`
AllowedIPs []string `gorm:"-"` // IPs that are used in the client config file AllowedIPs []string `gorm:"-"` // IPs that are used in the client config file
IPs []string `gorm:"-"` // The IPs of the client IPs []string `gorm:"-"` // The IPs of the client
DNSStr string DNSStr string `form:"dns" binding:"iplist"`
DNS []string `gorm:"-"` // The DNS servers of the client DNS []string `gorm:"-"` // The DNS servers of the client
PreUp string PreUp string `form:"preup"`
PostUp string PostUp string `form:"postup"`
PreDown string PreDown string `form:"predown"`
PostDown string PostDown string `form:"postdown"`
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
} }