diff --git a/assets/css/custom.css b/assets/css/custom.css index f450dbb..5a7d603 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -80,4 +80,24 @@ pre{background:#f7f7f9}iframe{overflow:hidden;border:none}@media (min-width: 768 .form-group.required label:after { content:"*"; color:red; +} + +a.advanced-settings:before { + content: "Hide"; +} + +a.advanced-settings.collapsed:before { + content: "Show"; +} + +.form-group.global-config label:after, .custom-control.global-config label:after { + content: "g"; + color: #0057bb; + font-size: xx-small; + top: -5px; + position: absolute; +} + +.text-blue { + color: #0057bb; } \ No newline at end of file diff --git a/assets/css/signin.css b/assets/css/signin.css index e69de29..8479bf2 100644 --- a/assets/css/signin.css +++ b/assets/css/signin.css @@ -0,0 +1,3 @@ +.navbar { + padding: 0.5rem 1rem; +} \ No newline at end of file diff --git a/assets/tpl/admin_create_clients.html b/assets/tpl/admin_create_clients.html index e2a3b06..5278323 100644 --- a/assets/tpl/admin_create_clients.html +++ b/assets/tpl/admin_create_clients.html @@ -24,13 +24,13 @@
- +
- +
diff --git a/assets/tpl/admin_edit_client.html b/assets/tpl/admin_edit_client.html index 4a429aa..7590957 100644 --- a/assets/tpl/admin_edit_client.html +++ b/assets/tpl/admin_edit_client.html @@ -13,34 +13,39 @@ {{template "prt_nav.html" .}}
+ {{template "prt_flashes.html" .}} + + + {{if eq .Device.Type "server"}} {{if .Peer.IsNew}}

Create a new client

{{else}} -

Edit client {{.Peer.Identifier}}

+

Edit client: {{.Peer.Identifier}}

{{end}} - {{template "prt_flashes.html" .}} -
+ + + {{if .EditableKeys}}
- - + +
- - + +
- - + +
{{else}} @@ -48,48 +53,64 @@
- - + +
{{end}}
- - + +
- - + +
- - + +
-
- - +
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+ +
- -
- -
@@ -99,6 +120,82 @@ Cancel + {{end}} + + + {{if eq .Device.Type "client"}} + {{if .Peer.IsNew}} +

Create a new remote endpoint

+ {{else}} +

Edit remote endpoint: {{.Peer.Identifier}}

+ {{end}} + +
+ + + + + + +
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + + + Cancel +
+ {{end}}
{{template "prt_footer.html" .}} diff --git a/assets/tpl/admin_edit_interface.html b/assets/tpl/admin_edit_interface.html index ca6add4..dfcce2c 100644 --- a/assets/tpl/admin_edit_interface.html +++ b/assets/tpl/admin_edit_interface.html @@ -16,99 +16,240 @@

Edit interface {{.Device.DeviceName}}

{{template "prt_flashes.html" .}} -
- - -

Server's interface configuration

- {{if .EditableKeys}} -
-
- - -
-
-
-
- - -
-
- {{else}} - -
-
- - -
-
- {{end}} -
-
- - -
-
- - -
-
-

Client's global configuration

-
-
- - -
-
-
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
-

Interface configuration hooks

-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
+ + +
+ +
+ + + + +

Server's interface configuration

+
+
+ + +
+
+ {{if .EditableKeys}} +
+
+ + +
+
+
+
+ + +
+
+ {{else}} + +
+
+ + +
+
+ {{end}} +
+
+ + +
+
+ + +
+
+

Client's global configuration (g)

+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+

Interface configuration hooks

+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+
+
+ + +
+
+
+
+ + + Cancel + Apply Global Settings (g) to clients +
- - Cancel - Apply Allowed IP's to clients - + +
+
+ + + +

Client's interface configuration

+
+
+ + +
+
+ {{if .EditableKeys}} +
+
+ + +
+
+
+
+ + +
+
+ {{else}} + +
+
+ + +
+
+ {{end}} +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+

Interface configuration hooks

+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + + Cancel +
+
+
{{template "prt_footer.html" .}} diff --git a/assets/tpl/admin_edit_user.html b/assets/tpl/admin_edit_user.html index e42de7a..80a7324 100644 --- a/assets/tpl/admin_edit_user.html +++ b/assets/tpl/admin_edit_user.html @@ -27,7 +27,7 @@
- +
{{else}} @@ -36,13 +36,13 @@
- +
- +
@@ -54,7 +54,7 @@
- +
diff --git a/assets/tpl/admin_index.html b/assets/tpl/admin_index.html index 06a53d1..089e6ad 100644 --- a/assets/tpl/admin_index.html +++ b/assets/tpl/admin_index.html @@ -18,7 +18,9 @@
- Interface status for {{.Device.DeviceName}} + Interface status for {{.Device.DeviceName}} {{if eq $.Device.Type "server"}}(server mode){{end}}{{if eq $.Device.Type "client"}}(client mode){{end}} + +         @@ -26,6 +28,7 @@
+ {{if eq $.Device.Type "server"}}
@@ -35,7 +38,7 @@ - + @@ -61,7 +64,7 @@ - + @@ -73,22 +76,68 @@ - +
Public Endpoint:{{.Device.Endpoint}}{{.Device.DefaultEndpoint}}
Listening Port:
Default allowed IP's:{{.Device.AllowedIPsStr}}{{.Device.DefaultAllowedIPsStr}}
Default DNS servers:
Default Keepalive Interval:{{.Device.PersistentKeepalive}}{{.Device.DefaultPersistentKeepalive}}
+ {{end}} + {{if eq $.Device.Type "client"}} +
+ + + + + + + + + + + + + + + +
Public Key:{{.Device.PublicKey}}
Enabled Endpoints:{{len .Device.Interface.Peers}}
Total Endpoints:{{.TotalPeers}}
+
+
+ + + + + + + + + + + + + + + +
IP Address:{{.Device.IPsStr}}
DNS servers:{{.Device.DNSStr}}
Default MTU:{{.Device.Mtu}}
+
+ {{end}}
+ {{if eq $.Device.Type "server"}}

Current VPN Peers

+ {{end}} + {{if eq $.Device.Type "client"}} +

Current VPN Endpoints

+ {{end}}
+ {{if eq $.Device.Type "server"}} - M + {{end}} + M
@@ -98,8 +147,15 @@ Identifier Public Key + {{if eq $.Device.Type "server"}} E-Mail + {{end}} + {{if eq $.Device.Type "server"}} IP's + {{end}} + {{if eq $.Device.Type "client"}} + Endpoint + {{end}} Handshake @@ -115,8 +171,15 @@ {{$p.Identifier}} {{$p.PublicKey}} + {{if eq $.Device.Type "server"}} {{$p.Email}} + {{end}} + {{if eq $.Device.Type "server"}} {{$p.IPsStr}} + {{end}} + {{if eq $.Device.Type "client"}} + {{$p.Endpoint}} + {{end}} {{$p.LastHandshake}} {{if eq $.Session.IsAdmin true}} @@ -133,9 +196,11 @@ + {{if eq $.Device.Type "server"}} + {{end}} @@ -161,22 +226,28 @@

{{if $p.DeactivatedAt}}-{{else}} {{formatBytes $p.Peer.ReceiveBytes}} / {{formatBytes $p.Peer.TransmitBytes}}{{end}}

{{end}}
+ {{if eq $.Device.Type "server"}}
{{$p.Config}}
+ {{end}}
+ {{if eq $.Device.Type "server"}} + {{end}}
+ {{if eq $.Device.Type "server"}} + {{end}}
diff --git a/assets/tpl/login.html b/assets/tpl/login.html index a6b6287..74e2bb0 100644 --- a/assets/tpl/login.html +++ b/assets/tpl/login.html @@ -13,8 +13,17 @@ - -
+ + +
Please sign in
@@ -28,15 +37,16 @@
- + {{ if eq .error true }} -
- {{.message}} + {{end}} -
+
diff --git a/assets/tpl/prt_nav.html b/assets/tpl/prt_nav.html index 994f540..e997a44 100644 --- a/assets/tpl/prt_nav.html +++ b/assets/tpl/prt_nav.html @@ -27,8 +27,8 @@
diff --git a/go.mod b/go.mod index 27c93e4..5a05f29 100644 --- a/go.mod +++ b/go.mod @@ -6,21 +6,21 @@ require ( github.com/gin-contrib/sessions v0.0.3 github.com/gin-gonic/gin v1.6.3 github.com/go-ldap/ldap/v3 v3.2.4 - github.com/go-playground/validator/v10 v10.2.0 + github.com/go-playground/validator/v10 v10.4.1 github.com/gorilla/sessions v1.2.1 // indirect github.com/jordan-wright/email v4.0.1-0.20200917010138-e1c00e156980+incompatible github.com/kelseyhightower/envconfig v1.4.0 github.com/milosgajdos/tenus v0.0.3 github.com/pkg/errors v0.9.1 - github.com/sirupsen/logrus v1.7.0 + github.com/sirupsen/logrus v1.8.1 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e - github.com/toorop/gin-logrus v0.0.0-20200831135515-d2ee50d38dae + github.com/toorop/gin-logrus v0.0.0-20210225092905-2c785434f26f github.com/utrack/gin-csrf v0.0.0-20190424104817-40fb8d2c8fca - golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 + golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20200609130330-bd2cb7843e1b - gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c - gorm.io/driver/mysql v1.0.4 - gorm.io/driver/sqlite v1.1.3 - gorm.io/gorm v1.20.12 + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b + gorm.io/driver/mysql v1.0.5 + gorm.io/driver/sqlite v1.1.4 + gorm.io/gorm v1.21.6 ) diff --git a/internal/common/db.go b/internal/common/db.go index 9261c58..7c8d5a1 100644 --- a/internal/common/db.go +++ b/internal/common/db.go @@ -38,7 +38,7 @@ func GetDatabaseForConfig(cfg *DatabaseConfig) (db *gorm.DB, err error) { return } } - db, err = gorm.Open(sqlite.Open(cfg.Database), &gorm.Config{}) + db, err = gorm.Open(sqlite.Open(cfg.Database), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true}) if err != nil { return } @@ -74,3 +74,36 @@ func GetDatabaseForConfig(cfg *DatabaseConfig) (db *gorm.DB, err error) { db.Config.Logger = logger.New(logrus.StandardLogger(), logCfg) return } + +type DatabaseMigrationInfo struct { + Version string `gorm:"primaryKey"` + Applied time.Time +} + +func MigrateDatabase(db *gorm.DB, version string) error { + if err := db.AutoMigrate(&DatabaseMigrationInfo{}); err != nil { + return errors.Wrap(err, "failed to migrate version database") + } + + newVersion := DatabaseMigrationInfo{ + Version: version, + Applied: time.Now(), + } + + existingMigration := DatabaseMigrationInfo{} + db.Where("version = ?", version).FirstOrInit(&existingMigration) + + if existingMigration.Version == "" { + lastVersion := DatabaseMigrationInfo{} + db.Order("applied desc, version desc").FirstOrInit(&lastVersion) + + // TODO: migrate database + + res := db.Create(&newVersion) + if res.Error != nil { + return errors.Wrap(res.Error, "failed to write version to database") + } + } + + return nil +} diff --git a/internal/server/configuration.go b/internal/server/configuration.go index bb985b0..2727967 100644 --- a/internal/server/configuration.go +++ b/internal/server/configuration.go @@ -85,6 +85,7 @@ func NewConfig() *Config { cfg.Core.AdminUser = "admin@wgportal.local" cfg.Core.AdminPassword = "wgportal" cfg.Core.LdapEnabled = false + cfg.Core.EditableKeys = true cfg.Core.SessionSecret = "secret" cfg.Database.Typ = "sqlite" diff --git a/internal/server/handlers_common.go b/internal/server/handlers_common.go index 84e028a..476474b 100644 --- a/internal/server/handlers_common.go +++ b/internal/server/handlers_common.go @@ -23,7 +23,7 @@ func (s *Server) GetHandleError(c *gin.Context, code int, message, details strin "Session": GetSessionData(c), "Static": s.getStaticData(), "Device": s.peers.GetDevice(currentSession.DeviceName), - "DeviceNames": s.wg.Cfg.DeviceNames, + "DeviceNames": s.GetDeviceNames(), }) } @@ -36,7 +36,7 @@ func (s *Server) GetIndex(c *gin.Context) { "Session": currentSession, "Static": s.getStaticData(), "Device": s.peers.GetDevice(currentSession.DeviceName), - "DeviceNames": s.wg.Cfg.DeviceNames, + "DeviceNames": s.GetDeviceNames(), }) } @@ -104,7 +104,7 @@ func (s *Server) GetAdminIndex(c *gin.Context) { "TotalPeers": len(s.peers.GetAllPeers(currentSession.DeviceName)), "Users": s.users.GetUsers(), "Device": device, - "DeviceNames": s.wg.Cfg.DeviceNames, + "DeviceNames": s.GetDeviceNames(), }) } @@ -143,7 +143,7 @@ func (s *Server) GetUserIndex(c *gin.Context) { "TotalPeers": len(peers), "Users": []users.User{*s.users.GetUser(currentSession.Email)}, "Device": s.peers.GetDevice(currentSession.DeviceName), - "DeviceNames": s.wg.Cfg.DeviceNames, + "DeviceNames": s.GetDeviceNames(), }) } @@ -160,6 +160,7 @@ func (s *Server) updateFormInSession(c *gin.Context, formData interface{}) error func (s *Server) setNewPeerFormInSession(c *gin.Context) (SessionData, error) { currentSession := GetSessionData(c) + // If session does not contain a peer form ignore update // If url contains a formerr parameter reset the form if currentSession.FormData == nil || c.Query("formerr") == "" { diff --git a/internal/server/handlers_interface.go b/internal/server/handlers_interface.go index 3b960f6..f22dec5 100644 --- a/internal/server/handlers_interface.go +++ b/internal/server/handlers_interface.go @@ -1,6 +1,7 @@ package server import ( + "fmt" "net/http" "strings" @@ -26,7 +27,7 @@ func (s *Server) GetAdminEditInterface(c *gin.Context) { "Static": s.getStaticData(), "Device": currentSession.FormData.(wireguard.Device), "EditableKeys": s.config.Core.EditableKeys, - "DeviceNames": s.wg.Cfg.DeviceNames, + "DeviceNames": s.GetDeviceNames(), "Csrf": csrf.GetToken(c), }) } @@ -44,12 +45,20 @@ func (s *Server) PostAdminEditInterface(c *gin.Context) { return } // Clean list input - formDevice.IPs = common.ParseStringList(formDevice.IPsStr) - formDevice.AllowedIPs = common.ParseStringList(formDevice.AllowedIPsStr) - formDevice.DNS = common.ParseStringList(formDevice.DNSStr) - formDevice.IPsStr = common.ListToString(formDevice.IPs) - formDevice.AllowedIPsStr = common.ListToString(formDevice.AllowedIPs) - formDevice.DNSStr = common.ListToString(formDevice.DNS) + formDevice.IPsStr = common.ListToString(common.ParseStringList(formDevice.IPsStr)) + formDevice.DefaultAllowedIPsStr = common.ListToString(common.ParseStringList(formDevice.DefaultAllowedIPsStr)) + formDevice.DNSStr = common.ListToString(common.ParseStringList(formDevice.DNSStr)) + + // Clean interface parameters based on interface type + switch formDevice.Type { + case wireguard.DeviceTypeClient: + formDevice.ListenPort = 0 + formDevice.DefaultEndpoint = "" + formDevice.DefaultAllowedIPsStr = "" + formDevice.DefaultPersistentKeepalive = 0 + formDevice.SaveConfig = false + case wireguard.DeviceTypeServer: + } // Update WireGuard device err := s.wg.UpdateDevice(formDevice.DeviceName, formDevice.GetConfig()) @@ -80,7 +89,7 @@ func (s *Server) PostAdminEditInterface(c *gin.Context) { // Update interface IP address if s.config.WG.ManageIPAddresses { - if err := s.wg.SetIPAddress(currentSession.DeviceName, formDevice.IPs); err != nil { + if err := s.wg.SetIPAddress(currentSession.DeviceName, formDevice.GetIPAddresses()); err != nil { _ = s.updateFormInSession(c, formDevice) SetFlashMessage(c, "Failed to update ip address: "+err.Error(), "danger") c.Redirect(http.StatusSeeOther, "/admin/device/edit?formerr=update") @@ -116,21 +125,53 @@ func (s *Server) GetInterfaceConfig(c *gin.Context) { return } +func (s *Server) GetSaveConfig(c *gin.Context) { + currentSession := GetSessionData(c) + + err := s.WriteWireGuardConfigFile(currentSession.DeviceName) + if err != nil { + SetFlashMessage(c, "Failed to save WireGuard config-file: "+err.Error(), "danger") + c.Redirect(http.StatusSeeOther, "/admin/") + return + } + + SetFlashMessage(c, "Updated WireGuard config-file", "success") + c.Redirect(http.StatusSeeOther, "/admin/") + return +} + func (s *Server) GetApplyGlobalConfig(c *gin.Context) { currentSession := GetSessionData(c) device := s.peers.GetDevice(currentSession.DeviceName) peers := s.peers.GetAllPeers(device.DeviceName) + if device.Type == wireguard.DeviceTypeClient { + SetFlashMessage(c, "Cannot apply global configuration while interface is in client mode.", "danger") + c.Redirect(http.StatusSeeOther, "/admin/device/edit") + return + } + + updateCounter := 0 for _, peer := range peers { - peer.AllowedIPs = device.AllowedIPs - peer.AllowedIPsStr = device.AllowedIPsStr + if peer.IgnoreGlobalSettings { + continue + } + + peer.AllowedIPsStr = device.DefaultAllowedIPsStr + peer.Endpoint = device.DefaultEndpoint + peer.PersistentKeepalive = device.DefaultPersistentKeepalive + peer.DNSStr = device.DNSStr + peer.Mtu = device.Mtu + if err := s.peers.UpdatePeer(peer); err != nil { SetFlashMessage(c, err.Error(), "danger") c.Redirect(http.StatusSeeOther, "/admin/device/edit") + return } + updateCounter++ } - SetFlashMessage(c, "Allowed IP's updated for all clients.", "success") + SetFlashMessage(c, fmt.Sprintf("Global configuration updated for %d clients.", updateCounter), "success") c.Redirect(http.StatusSeeOther, "/admin/device/edit") return } diff --git a/internal/server/handlers_peer.go b/internal/server/handlers_peer.go index 2b9211d..3933519 100644 --- a/internal/server/handlers_peer.go +++ b/internal/server/handlers_peer.go @@ -39,7 +39,8 @@ func (s *Server) GetAdminEditPeer(c *gin.Context) { "Peer": currentSession.FormData.(wireguard.Peer), "EditableKeys": s.config.Core.EditableKeys, "Device": s.peers.GetDevice(currentSession.DeviceName), - "DeviceNames": s.wg.Cfg.DeviceNames, + "DeviceNames": s.GetDeviceNames(), + "AdminEmail": s.config.Core.AdminUser, "Csrf": csrf.GetToken(c), }) } @@ -61,10 +62,8 @@ func (s *Server) PostAdminEditPeer(c *gin.Context) { } // Clean list input - formPeer.IPs = common.ParseStringList(formPeer.IPsStr) - formPeer.AllowedIPs = common.ParseStringList(formPeer.AllowedIPsStr) - formPeer.IPsStr = common.ListToString(formPeer.IPs) - formPeer.AllowedIPsStr = common.ListToString(formPeer.AllowedIPs) + formPeer.IPsStr = common.ListToString(common.ParseStringList(formPeer.IPsStr)) + formPeer.AllowedIPsStr = common.ListToString(common.ParseStringList(formPeer.AllowedIPsStr)) disabled := c.PostForm("isdisabled") != "" now := time.Now() @@ -100,7 +99,8 @@ func (s *Server) GetAdminCreatePeer(c *gin.Context) { "Peer": currentSession.FormData.(wireguard.Peer), "EditableKeys": s.config.Core.EditableKeys, "Device": s.peers.GetDevice(currentSession.DeviceName), - "DeviceNames": s.wg.Cfg.DeviceNames, + "DeviceNames": s.GetDeviceNames(), + "AdminEmail": s.config.Core.AdminUser, "Csrf": csrf.GetToken(c), }) } @@ -119,10 +119,8 @@ func (s *Server) PostAdminCreatePeer(c *gin.Context) { } // Clean list input - formPeer.IPs = common.ParseStringList(formPeer.IPsStr) - formPeer.AllowedIPs = common.ParseStringList(formPeer.AllowedIPsStr) - formPeer.IPsStr = common.ListToString(formPeer.IPs) - formPeer.AllowedIPsStr = common.ListToString(formPeer.AllowedIPs) + formPeer.IPsStr = common.ListToString(common.ParseStringList(formPeer.IPsStr)) + formPeer.AllowedIPsStr = common.ListToString(common.ParseStringList(formPeer.AllowedIPsStr)) disabled := c.PostForm("isdisabled") != "" now := time.Now() @@ -156,7 +154,7 @@ func (s *Server) GetAdminCreateLdapPeers(c *gin.Context) { "Users": s.users.GetFilteredAndSortedUsers("lastname", "asc", ""), "FormData": currentSession.FormData.(LdapCreateForm), "Device": s.peers.GetDevice(currentSession.DeviceName), - "DeviceNames": s.wg.Cfg.DeviceNames, + "DeviceNames": s.GetDeviceNames(), "Csrf": csrf.GetToken(c), }) } @@ -177,7 +175,7 @@ func (s *Server) PostAdminCreateLdapPeers(c *gin.Context) { emails := common.ParseStringList(formData.Emails) for i := range emails { // TODO: also check email addr for validity? - if !strings.ContainsRune(emails[i], '@') || s.users.GetUser(emails[i]) == nil { + if !strings.ContainsRune(emails[i], '@') { _ = s.updateFormInSession(c, formData) SetFlashMessage(c, "invalid email address: "+emails[i], "danger") c.Redirect(http.StatusSeeOther, "/admin/peer/createldap?formerr=mail") @@ -328,7 +326,7 @@ func (s *Server) GetPeerStatus(c *gin.Context) { isOnline := false ping := make(chan bool) defer close(ping) - for _, cidr := range peer.IPs { + for _, cidr := range peer.GetIPAddresses() { ip, _, _ := net.ParseCIDR(cidr) var ra *net.IPAddr if common.IsIPv6(ip.String()) { diff --git a/internal/server/handlers_user.go b/internal/server/handlers_user.go index 17e9ff4..643e56f 100644 --- a/internal/server/handlers_user.go +++ b/internal/server/handlers_user.go @@ -58,7 +58,7 @@ func (s *Server) GetAdminUsersIndex(c *gin.Context) { "Users": dbUsers, "TotalUsers": len(s.users.GetUsers()), "Device": s.peers.GetDevice(currentSession.DeviceName), - "DeviceNames": s.wg.Cfg.DeviceNames, + "DeviceNames": s.GetDeviceNames(), }) } @@ -78,7 +78,7 @@ func (s *Server) GetAdminUsersEdit(c *gin.Context) { "Static": s.getStaticData(), "User": currentSession.FormData.(users.User), "Device": s.peers.GetDevice(currentSession.DeviceName), - "DeviceNames": s.wg.Cfg.DeviceNames, + "DeviceNames": s.GetDeviceNames(), "Epoch": time.Time{}, "Csrf": csrf.GetToken(c), }) @@ -156,7 +156,7 @@ func (s *Server) GetAdminUsersCreate(c *gin.Context) { "Static": s.getStaticData(), "User": currentSession.FormData.(users.User), "Device": s.peers.GetDevice(currentSession.DeviceName), - "DeviceNames": s.wg.Cfg.DeviceNames, + "DeviceNames": s.GetDeviceNames(), "Epoch": time.Time{}, "Csrf": csrf.GetToken(c), }) diff --git a/internal/server/routes.go b/internal/server/routes.go index 03cf998..6eb47ac 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -32,6 +32,7 @@ func SetupRoutes(s *Server) { admin.GET("/device/edit", s.GetAdminEditInterface) admin.POST("/device/edit", s.PostAdminEditInterface) admin.GET("/device/download", s.GetInterfaceConfig) + admin.GET("/device/write", s.GetSaveConfig) admin.GET("/device/applyglobals", s.GetApplyGlobalConfig) admin.GET("/peer/edit", s.GetAdminEditPeer) admin.POST("/peer/edit", s.PostAdminEditPeer) diff --git a/internal/server/server.go b/internal/server/server.go index 432dd2d..f2cde1e 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -103,6 +103,10 @@ func (s *Server) Setup(ctx context.Context) error { if err != nil { return errors.WithMessage(err, "database setup failed") } + err = common.MigrateDatabase(s.db, Version) + if err != nil { + return errors.WithMessage(err, "database migration failed") + } // Setup http server gin.SetMode(gin.DebugMode) @@ -183,9 +187,6 @@ func (s *Server) Setup(ctx context.Context) error { if s.peers, err = wireguard.NewPeerManager(s.db, s.wg); err != nil { return errors.WithMessage(err, "unable to setup peer manager") } - if err = s.peers.InitFromPhysicalInterface(); err != nil { - return errors.WithMessagef(err, "unable to initialize peer manager") - } for _, deviceName := range s.wg.Cfg.DeviceNames { if err = s.RestoreWireGuardInterface(deviceName); err != nil { diff --git a/internal/server/server_helper.go b/internal/server/server_helper.go index cbe3e9c..5eca32d 100644 --- a/internal/server/server_helper.go +++ b/internal/server/server_helper.go @@ -8,7 +8,6 @@ import ( "syscall" "time" - "github.com/h44z/wg-portal/internal/common" "github.com/h44z/wg-portal/internal/users" "github.com/h44z/wg-portal/internal/wireguard" "github.com/pkg/errors" @@ -20,50 +19,60 @@ import ( // PrepareNewPeer initiates a new peer for the given WireGuard device. func (s *Server) PrepareNewPeer(device string) (wireguard.Peer, error) { dev := s.peers.GetDevice(device) + deviceIPs := dev.GetIPAddresses() peer := wireguard.Peer{} peer.IsNew = true - peer.AllowedIPsStr = dev.AllowedIPsStr - peer.IPs = make([]string, len(dev.IPs)) - for i := range dev.IPs { - freeIP, err := s.peers.GetAvailableIp(device, dev.IPs[i]) - if err != nil { - return wireguard.Peer{}, errors.WithMessage(err, "failed to get available IP addresses") + + switch dev.Type { + case wireguard.DeviceTypeServer: + peerIPs := make([]string, len(deviceIPs)) + for i := range deviceIPs { + freeIP, err := s.peers.GetAvailableIp(device, deviceIPs[i]) + if err != nil { + return wireguard.Peer{}, errors.WithMessage(err, "failed to get available IP addresses") + } + peerIPs[i] = freeIP } - peer.IPs[i] = freeIP + peer.SetIPAddresses(peerIPs...) + psk, err := wgtypes.GenerateKey() + if err != nil { + return wireguard.Peer{}, errors.Wrap(err, "failed to generate key") + } + key, err := wgtypes.GeneratePrivateKey() + if err != nil { + return wireguard.Peer{}, errors.Wrap(err, "failed to generate private key") + } + peer.PresharedKey = psk.String() + peer.PrivateKey = key.String() + peer.PublicKey = key.PublicKey().String() + peer.UID = fmt.Sprintf("u%x", md5.Sum([]byte(peer.PublicKey))) + peer.Endpoint = dev.DefaultEndpoint + peer.DNSStr = dev.DNSStr + peer.PersistentKeepalive = dev.DefaultPersistentKeepalive + peer.AllowedIPsStr = dev.DefaultAllowedIPsStr + peer.Mtu = dev.Mtu + case wireguard.DeviceTypeClient: + peer.UID = "newendpoint" } - peer.IPsStr = common.ListToString(peer.IPs) - psk, err := wgtypes.GenerateKey() - if err != nil { - return wireguard.Peer{}, errors.Wrap(err, "failed to generate key") - } - key, err := wgtypes.GeneratePrivateKey() - if err != nil { - return wireguard.Peer{}, errors.Wrap(err, "failed to generate private key") - } - peer.PresharedKey = psk.String() - peer.PrivateKey = key.String() - peer.PublicKey = key.PublicKey().String() - peer.UID = fmt.Sprintf("u%x", md5.Sum([]byte(peer.PublicKey))) return peer, nil } -// CreatePeerByEmail creates a new peer for the given email. If no user with the specified email was found, a new one -// will be created. +// CreatePeerByEmail creates a new peer for the given email. func (s *Server) CreatePeerByEmail(device, email, identifierSuffix string, disabled bool) error { - user, err := s.users.GetOrCreateUser(email) - if err != nil { - return errors.WithMessagef(err, "failed to load/create related user %s", email) - } + user := s.users.GetUser(email) peer, err := s.PrepareNewPeer(device) if err != nil { return errors.WithMessage(err, "failed to prepare new peer") } peer.Email = email - peer.Identifier = fmt.Sprintf("%s %s (%s)", user.Firstname, user.Lastname, identifierSuffix) - + if user != nil { + peer.Identifier = fmt.Sprintf("%s %s (%s)", user.Firstname, user.Lastname, identifierSuffix) + } else { + peer.Identifier = fmt.Sprintf("%s (%s)", email, identifierSuffix) + } now := time.Now() if disabled { peer.DeactivatedAt = &now @@ -77,19 +86,22 @@ func (s *Server) CreatePeerByEmail(device, email, identifierSuffix string, disab // This function also configures the new peer on the physical WireGuard interface if the peer is not deactivated. func (s *Server) CreatePeer(device string, peer wireguard.Peer) error { dev := s.peers.GetDevice(device) - peer.AllowedIPsStr = dev.AllowedIPsStr - if peer.IPs == nil || len(peer.IPs) == 0 { - peer.IPs = make([]string, len(dev.IPs)) - for i := range dev.IPs { - freeIP, err := s.peers.GetAvailableIp(device, dev.IPs[i]) + deviceIPs := dev.GetIPAddresses() + peerIPs := peer.GetIPAddresses() + + peer.AllowedIPsStr = dev.DefaultAllowedIPsStr + if len(peerIPs) == 0 && dev.Type == wireguard.DeviceTypeServer { + peerIPs = make([]string, len(deviceIPs)) + for i := range deviceIPs { + freeIP, err := s.peers.GetAvailableIp(device, deviceIPs[i]) if err != nil { return errors.WithMessage(err, "failed to get available IP addresses") } - peer.IPs[i] = freeIP + peerIPs[i] = freeIP } - peer.IPsStr = common.ListToString(peer.IPs) + peer.SetIPAddresses(peerIPs...) } - if peer.PrivateKey == "" { // if private key is empty create a new one + if peer.PrivateKey == "" && dev.Type == wireguard.DeviceTypeServer { // if private key is empty create a new one psk, err := wgtypes.GenerateKey() if err != nil { return errors.Wrap(err, "failed to generate key") @@ -107,7 +119,7 @@ func (s *Server) CreatePeer(device string, peer wireguard.Peer) error { // Create WireGuard interface if peer.DeactivatedAt == nil { - if err := s.wg.AddPeer(device, peer.GetConfig()); err != nil { + if err := s.wg.AddPeer(device, peer.GetConfig(&dev)); err != nil { return errors.WithMessage(err, "failed to add WireGuard peer") } } @@ -123,21 +135,24 @@ func (s *Server) CreatePeer(device string, peer wireguard.Peer) error { // UpdatePeer updates the physical WireGuard interface and the database. func (s *Server) UpdatePeer(peer wireguard.Peer, updateTime time.Time) error { currentPeer := s.peers.GetPeerByKey(peer.PublicKey) + dev := s.peers.GetDevice(peer.DeviceName) // Update WireGuard device var err error switch { - case peer.DeactivatedAt == &updateTime: + case peer.DeactivatedAt != nil && *peer.DeactivatedAt == updateTime: err = s.wg.RemovePeer(peer.DeviceName, peer.PublicKey) case peer.DeactivatedAt == nil && currentPeer.Peer != nil: - err = s.wg.UpdatePeer(peer.DeviceName, peer.GetConfig()) + err = s.wg.UpdatePeer(peer.DeviceName, peer.GetConfig(&dev)) case peer.DeactivatedAt == nil && currentPeer.Peer == nil: - err = s.wg.AddPeer(peer.DeviceName, peer.GetConfig()) + err = s.wg.AddPeer(peer.DeviceName, peer.GetConfig(&dev)) } if err != nil { return errors.WithMessage(err, "failed to update WireGuard peer") } + peer.UID = fmt.Sprintf("u%x", md5.Sum([]byte(peer.PublicKey))) + // Update in database if err := s.peers.UpdatePeer(peer); err != nil { return errors.WithMessage(err, "failed to update peer") @@ -164,10 +179,11 @@ func (s *Server) DeletePeer(peer wireguard.Peer) error { // RestoreWireGuardInterface restores the state of the physical WireGuard interface from the database. func (s *Server) RestoreWireGuardInterface(device string) error { activePeers := s.peers.GetActivePeers(device) + dev := s.peers.GetDevice(device) for i := range activePeers { if activePeers[i].Peer == nil { - if err := s.wg.AddPeer(device, activePeers[i].GetConfig()); err != nil { + if err := s.wg.AddPeer(device, activePeers[i].GetConfig(&dev)); err != nil { return errors.WithMessage(err, "failed to add WireGuard peer") } } @@ -294,3 +310,14 @@ func (s *Server) CreateUserDefaultPeer(email, device string) error { return nil } + +func (s *Server) GetDeviceNames() map[string]string { + devNames := make(map[string]string, len(s.wg.Cfg.DeviceNames)) + + for _, devName := range s.wg.Cfg.DeviceNames { + dev := s.peers.GetDevice(devName) + devNames[devName] = dev.DisplayName + } + + return devNames +} diff --git a/internal/server/version.go b/internal/server/version.go new file mode 100644 index 0000000..29b597f --- /dev/null +++ b/internal/server/version.go @@ -0,0 +1,3 @@ +package server + +var Version = "1.0.5" diff --git a/internal/wireguard/peermanager.go b/internal/wireguard/peermanager.go index 11217dc..aec3d39 100644 --- a/internal/wireguard/peermanager.go +++ b/internal/wireguard/peermanager.go @@ -1,17 +1,19 @@ package wireguard +// WireGuard documentation: https://manpages.debian.org/unstable/wireguard-tools/wg.8.en.html + import ( "bytes" "crypto/md5" "fmt" "net" - "reflect" "regexp" "sort" "strings" - "text/template" "time" + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" "github.com/go-playground/validator/v10" "github.com/h44z/wg-portal/internal/common" @@ -40,7 +42,6 @@ var cidrList validator.Func = func(fl validator.FieldLevel) bool { var ipList validator.Func = func(fl validator.FieldLevel) bool { ipListStr := fl.Field().String() - ipList := common.ParseStringList(ipListStr) for i := range ipList { ip := net.ParseIP(ipList[i]) @@ -63,26 +64,35 @@ func init() { // type Peer struct { - Peer *wgtypes.Peer `gorm:"-"` // WireGuard peer + Peer *wgtypes.Peer `gorm:"-"` // WireGuard peer + Device *Device `gorm:"foreignKey:DeviceName" binding:"-"` // linked WireGuard device Config string `gorm:"-"` - UID string `form:"uid" binding:"alphanum"` // uid for html identification + UID string `form:"uid" binding:"required,alphanum"` // uid for html identification + DeviceName string `gorm:"index" form:"device" binding:"required"` + DeviceType DeviceType `gorm:"-" form:"devicetype" binding:"required,oneof=client server"` + Identifier string `form:"identifier" binding:"required,max=64"` // Identifier AND Email make a WireGuard peer unique + Email string `gorm:"index" form:"mail" binding:"required,email"` + IgnoreGlobalSettings bool `form:"ignoreglobalsettings"` + IsOnline bool `gorm:"-"` IsNew bool `gorm:"-"` - Identifier string `form:"identifier" binding:"required,lt=64"` // Identifier AND Email make a WireGuard peer unique - Email string `gorm:"index" form:"mail" binding:"required,email"` LastHandshake string `gorm:"-"` LastHandshakeTime string `gorm:"-"` - IgnorePersistentKeepalive bool `form:"ignorekeepalive"` - PresharedKey string `form:"presharedkey" binding:"omitempty,base64"` - AllowedIPsStr string `form:"allowedip" binding:"cidrlist"` - IPsStr string `form:"ip" binding:"cidrlist"` - AllowedIPs []string `gorm:"-"` // IPs that are used in the client config file - IPs []string `gorm:"-"` // The IPs of the client - PrivateKey string `form:"privkey" binding:"omitempty,base64"` - PublicKey string `gorm:"primaryKey" form:"pubkey" binding:"required,base64"` - DeviceName string `gorm:"index"` + // Core WireGuard Settings + PublicKey string `gorm:"primaryKey" form:"pubkey" binding:"required,base64"` // the public key of the peer itself + PresharedKey string `form:"presharedkey" binding:"omitempty,base64"` + AllowedIPsStr string `form:"allowedip" binding:"cidrlist"` // a comma separated list of IPs that are used in the client config file + Endpoint string `form:"endpoint" binding:"omitempty,hostname_port"` + PersistentKeepalive int `form:"keepalive" binding:"gte=0"` + + // Misc. WireGuard Settings + PrivateKey string `form:"privkey" binding:"omitempty,base64"` + IPsStr string `form:"ip" binding:"cidrlist,required_if=DeviceType server"` // a comma separated list of IPs of the client + DNSStr string `form:"dns" binding:"iplist"` // comma separated list of the DNS servers for the client + // Global Device Settings (can be ignored, only make sense if device is in server mode) + Mtu int `form:"mtu" binding:"gte=0,lte=1500"` DeactivatedAt *time.Time CreatedBy string @@ -91,48 +101,82 @@ type Peer struct { UpdatedAt time.Time } -func (p Peer) GetConfig() wgtypes.PeerConfig { +func (p *Peer) SetIPAddresses(addresses ...string) { + p.IPsStr = common.ListToString(addresses) +} + +func (p Peer) GetIPAddresses() []string { + return common.ParseStringList(p.IPsStr) +} + +func (p *Peer) SetDNSServers(addresses ...string) { + p.DNSStr = common.ListToString(addresses) +} + +func (p Peer) GetDNSServers() []string { + return common.ParseStringList(p.DNSStr) +} + +func (p *Peer) SetAllowedIPs(addresses ...string) { + p.AllowedIPsStr = common.ListToString(addresses) +} + +func (p Peer) GetAllowedIPs() []string { + return common.ParseStringList(p.AllowedIPsStr) +} + +func (p Peer) GetConfig(_ *Device) wgtypes.PeerConfig { publicKey, _ := wgtypes.ParseKey(p.PublicKey) + var presharedKey *wgtypes.Key if p.PresharedKey != "" { presharedKeyTmp, _ := wgtypes.ParseKey(p.PresharedKey) presharedKey = &presharedKeyTmp } + var endpoint *net.UDPAddr + if p.Endpoint != "" { + addr, err := net.ResolveUDPAddr("udp", p.Endpoint) + if err == nil { + endpoint = addr + } + } + + var keepAlive *time.Duration + if p.PersistentKeepalive != 0 { + keepAliveDuration := time.Duration(p.PersistentKeepalive) * time.Second + keepAlive = &keepAliveDuration + } + + peerAllowedIPs := p.GetAllowedIPs() + allowedIPs := make([]net.IPNet, len(peerAllowedIPs)) + for i, ip := range peerAllowedIPs { + _, ipNet, err := net.ParseCIDR(ip) + if err == nil { + allowedIPs[i] = *ipNet + } + } + cfg := wgtypes.PeerConfig{ PublicKey: publicKey, Remove: false, UpdateOnly: false, PresharedKey: presharedKey, - Endpoint: nil, - PersistentKeepaliveInterval: nil, + Endpoint: endpoint, + PersistentKeepaliveInterval: keepAlive, ReplaceAllowedIPs: true, - AllowedIPs: make([]net.IPNet, len(p.IPs)), - } - for i, ip := range p.IPs { - _, ipNet, err := net.ParseCIDR(ip) - if err == nil { - cfg.AllowedIPs[i] = *ipNet - } + AllowedIPs: allowedIPs, } return cfg } func (p Peer) GetConfigFile(device Device) ([]byte, error) { - tpl, err := template.New("client").Funcs(template.FuncMap{"StringsJoin": strings.Join}).Parse(ClientCfgTpl) - if err != nil { - return nil, errors.Wrap(err, "failed to parse client template") - } - var tplBuff bytes.Buffer - err = tpl.Execute(&tplBuff, struct { - Client Peer - Server Device - }{ - Client: p, - Server: device, + err := templateCache.ExecuteTemplate(&tplBuff, "peer.tpl", gin.H{ + "Peer": p, + "Interface": device, }) if err != nil { return nil, errors.Wrap(err, "failed to execute client template") @@ -160,26 +204,6 @@ func (p Peer) IsValid() bool { return true } -func (p Peer) ToMap() map[string]string { - out := make(map[string]string) - - v := reflect.ValueOf(p) - if v.Kind() == reflect.Ptr { - v = v.Elem() - } - - typ := v.Type() - for i := 0; i < v.NumField(); i++ { - // gets us a StructField - fi := typ.Field(i) - if tagv := fi.Tag.Get("form"); tagv != "" { - // set key of map to value in struct field - out[tagv] = v.Field(i).String() - } - } - return out -} - func (p Peer) GetConfigFileName() string { reg := regexp.MustCompile("[^a-zA-Z0-9_-]+") return reg.ReplaceAllString(strings.ReplaceAll(p.Identifier, " ", "-"), "") + ".conf" @@ -189,44 +213,93 @@ func (p Peer) GetConfigFileName() string { // DEVICE -------------------------------------------------------------------------------------- // +type DeviceType string + +const ( + DeviceTypeServer DeviceType = "server" + DeviceTypeClient DeviceType = "client" +) + type Device struct { Interface *wgtypes.Device `gorm:"-"` - DeviceName string `form:"device" gorm:"primaryKey" binding:"required,alphanum"` - PrivateKey string `form:"privkey" binding:"required,base64"` - PublicKey string `form:"pubkey" binding:"required,base64"` - PersistentKeepalive int `form:"keepalive" binding:"gte=0"` - ListenPort int `form:"port" binding:"required,gt=0"` - Mtu int `form:"mtu" binding:"gte=0,lte=1500"` - Endpoint string `form:"endpoint" binding:"required,hostname_port"` - AllowedIPsStr string `form:"allowedip" binding:"cidrlist"` - IPsStr string `form:"ip" binding:"required,cidrlist"` - AllowedIPs []string `gorm:"-"` // IPs that are used in the client config file - IPs []string `gorm:"-"` // The IPs of the client - DNSStr string `form:"dns" binding:"iplist"` - DNS []string `gorm:"-"` // The DNS servers of the client - PreUp string `form:"preup"` - PostUp string `form:"postup"` - PreDown string `form:"predown"` - PostDown string `form:"postdown"` - CreatedAt time.Time - UpdatedAt time.Time + Type DeviceType `form:"devicetype" binding:"required,oneof=client server"` + DeviceName string `form:"device" gorm:"primaryKey" binding:"required,alphanum"` + DisplayName string `form:"displayname" binding:"omitempty,max=200"` + + // Core WireGuard Settings (Interface section) + PrivateKey string `form:"privkey" binding:"required,base64"` + ListenPort int `form:"port" binding:"required_if=Type server,omitempty,gt=0,lt=65535"` + FirewallMark int32 `form:"firewallmark" binding:"gte=0"` + // Misc. WireGuard Settings + PublicKey string `form:"pubkey" binding:"required,base64"` + Mtu int `form:"mtu" binding:"gte=0,lte=1500"` // the interface MTU, wg-quick addition + IPsStr string `form:"ip" binding:"required,cidrlist"` // comma separated list of the IPs of the client, wg-quick addition + DNSStr string `form:"dns" binding:"iplist"` // comma separated list of the DNS servers of the client, wg-quick addition + RoutingTable string `form:"routingtable"` // the routing table, wg-quick addition + PreUp string `form:"preup"` // pre up script, wg-quick addition + PostUp string `form:"postup"` // post up script, wg-quick addition + PreDown string `form:"predown"` // pre down script, wg-quick addition + PostDown string `form:"postdown"` // post down script, wg-quick addition + SaveConfig bool `form:"saveconfig"` // if set to `true', the configuration is saved from the current state of the interface upon shutdown, wg-quick addition + + // Settings that are applied to all peer by default + DefaultEndpoint string `form:"endpoint" binding:"required_if=Type server,omitempty,hostname_port"` + DefaultAllowedIPsStr string `form:"allowedip" binding:"cidrlist"` // comma separated list of IPs that are used in the client config file + DefaultPersistentKeepalive int `form:"keepalive" binding:"gte=0"` + + CreatedAt time.Time + UpdatedAt time.Time } func (d Device) IsValid() bool { - if d.PublicKey == "" { - return false - } - if len(d.IPs) == 0 { - return false - } - if d.Endpoint == "" { - return false + switch d.Type { + case DeviceTypeServer: + if d.PublicKey == "" { + return false + } + if len(d.GetIPAddresses()) == 0 { + return false + } + if d.DefaultEndpoint == "" { + return false + } + case DeviceTypeClient: + if d.PublicKey == "" { + return false + } + if len(d.GetIPAddresses()) == 0 { + return false + } } return true } +func (d *Device) SetIPAddresses(addresses ...string) { + d.IPsStr = common.ListToString(addresses) +} + +func (d Device) GetIPAddresses() []string { + return common.ParseStringList(d.IPsStr) +} + +func (d *Device) SetDNSServers(addresses ...string) { + d.DNSStr = common.ListToString(addresses) +} + +func (d Device) GetDNSServers() []string { + return common.ParseStringList(d.DNSStr) +} + +func (d *Device) SetDefaultAllowedIPs(addresses ...string) { + d.DefaultAllowedIPsStr = common.ListToString(addresses) +} + +func (d Device) GetDefaultAllowedIPs() []string { + return common.ParseStringList(d.DefaultAllowedIPsStr) +} + func (d Device) GetConfig() wgtypes.Config { var privateKey *wgtypes.Key if d.PrivateKey != "" { @@ -234,28 +307,23 @@ func (d Device) GetConfig() wgtypes.Config { privateKey = &pKey } + fwMark := int(d.FirewallMark) + cfg := wgtypes.Config{ - PrivateKey: privateKey, - ListenPort: &d.ListenPort, + PrivateKey: privateKey, + ListenPort: &d.ListenPort, + FirewallMark: &fwMark, } return cfg } func (d Device) GetConfigFile(peers []Peer) ([]byte, error) { - tpl, err := template.New("server").Funcs(template.FuncMap{"StringsJoin": strings.Join}).Parse(DeviceCfgTpl) - if err != nil { - return nil, errors.Wrap(err, "failed to parse server template") - } - var tplBuff bytes.Buffer - err = tpl.Execute(&tplBuff, struct { - Clients []Peer - Server Device - }{ - Clients: peers, - Server: d, + err := templateCache.ExecuteTemplate(&tplBuff, "interface.tpl", gin.H{ + "Peers": peers, + "Interface": d, }) if err != nil { return nil, errors.Wrap(err, "failed to execute server template") @@ -276,10 +344,31 @@ type PeerManager struct { func NewPeerManager(db *gorm.DB, wg *Manager) (*PeerManager, error) { pm := &PeerManager{db: db, wg: wg} + // check if old device table exists (from version <= 1.0.3), if so migrate it. + if db.Migrator().HasColumn(&Device{}, "endpoint") { + if err := db.Migrator().RenameColumn(&Device{}, "endpoint", "default_endpoint"); err != nil { + return nil, errors.Wrapf(err, "failed to migrate old database structure for column endpoint") + } + } + if db.Migrator().HasColumn(&Device{}, "allowed_ips_str") { + if err := db.Migrator().RenameColumn(&Device{}, "allowed_ips_str", "default_allowed_ips_str"); err != nil { + return nil, errors.Wrapf(err, "failed to migrate old database structure for column allowed_ips_str") + } + } + if db.Migrator().HasColumn(&Device{}, "persistent_keepalive") { + if err := db.Migrator().RenameColumn(&Device{}, "persistent_keepalive", "default_persistent_keepalive"); err != nil { + return nil, errors.Wrapf(err, "failed to migrate old database structure for column persistent_keepalive") + } + } + if err := pm.db.AutoMigrate(&Peer{}, &Device{}); err != nil { return nil, errors.WithMessage(err, "failed to migrate peer database") } + if err := pm.initFromPhysicalInterface(); err != nil { + return nil, errors.WithMessagef(err, "unable to initialize peer manager") + } + // check if peers without device name exist (from version <= 1.0.3), if so assign them to the default device. peers := make([]Peer, 0) pm.db.Find(&peers) @@ -290,12 +379,23 @@ func NewPeerManager(db *gorm.DB, wg *Manager) (*PeerManager, error) { } } + // validate and update existing peers if needed + for _, deviceName := range wg.Cfg.DeviceNames { + dev := pm.GetDevice(deviceName) + peers := pm.GetAllPeers(deviceName) + for i := range peers { + if err := pm.fixPeerDefaultData(&peers[i], &dev); err != nil { + return nil, errors.WithMessagef(err, "unable to fix peers for interface %s", deviceName) + } + } + } + return pm, nil } -// InitFromPhysicalInterface read all WireGuard peers from the WireGuard interface configuration. If a peer does not +// initFromPhysicalInterface read all WireGuard peers from the WireGuard interface configuration. If a peer does not // exist in the local database, it gets created. -func (m *PeerManager) InitFromPhysicalInterface() error { +func (m *PeerManager) initFromPhysicalInterface() error { for _, deviceName := range m.wg.Cfg.DeviceNames { peers, err := m.wg.GetPeerList(deviceName) if err != nil { @@ -316,43 +416,53 @@ func (m *PeerManager) InitFromPhysicalInterface() error { } } - // Check if entries already exist in database, if not create them + // Check if device already exists in database, if not, create it + if err := m.validateOrCreateDevice(*device, ipAddresses, mtu); err != nil { + return errors.WithMessagef(err, "failed to validate device %s", device.Name) + } + + // Check if entries already exist in database, if not, create them for _, peer := range peers { if err := m.validateOrCreatePeer(deviceName, peer); err != nil { return errors.WithMessagef(err, "failed to validate peer %s for device %s", peer.PublicKey, deviceName) } } - if err := m.validateOrCreateDevice(*device, ipAddresses, mtu); err != nil { - return errors.WithMessagef(err, "failed to validate device %s", device.Name) - } } return nil } // validateOrCreatePeer checks if the given WireGuard peer already exists in the database, if not, the peer entry will be created +// assumption: server mode is used func (m *PeerManager) validateOrCreatePeer(device string, wgPeer wgtypes.Peer) error { peer := Peer{} m.db.Where("public_key = ?", wgPeer.PublicKey.String()).FirstOrInit(&peer) + dev := m.GetDevice(device) + if peer.PublicKey == "" { // peer not found, create peer.UID = fmt.Sprintf("u%x", md5.Sum([]byte(wgPeer.PublicKey.String()))) - peer.PublicKey = wgPeer.PublicKey.String() - peer.PrivateKey = "" // UNKNOWN + if dev.Type == DeviceTypeServer { + peer.PublicKey = wgPeer.PublicKey.String() + peer.Identifier = "Autodetected Client (" + peer.PublicKey[0:8] + ")" + } else if dev.Type == DeviceTypeClient { + peer.PublicKey = wgPeer.PublicKey.String() + if wgPeer.Endpoint != nil { + peer.Endpoint = wgPeer.Endpoint.String() + } + peer.Identifier = "Autodetected Endpoint (" + peer.PublicKey[0:8] + ")" + } if wgPeer.PresharedKey != (wgtypes.Key{}) { peer.PresharedKey = wgPeer.PresharedKey.String() } peer.Email = "autodetected@example.com" - peer.Identifier = "Autodetected (" + peer.PublicKey[0:8] + ")" peer.UpdatedAt = time.Now() peer.CreatedAt = time.Now() - peer.AllowedIPs = make([]string, 0) // UNKNOWN - peer.IPs = make([]string, len(wgPeer.AllowedIPs)) + IPs := make([]string, len(wgPeer.AllowedIPs)) // use allowed IP's as the peer IP's for i, ip := range wgPeer.AllowedIPs { - peer.IPs[i] = ip.String() + IPs[i] = ip.String() } - peer.AllowedIPsStr = strings.Join(peer.AllowedIPs, ", ") - peer.IPsStr = strings.Join(peer.IPs, ", ") + peer.SetIPAddresses(IPs...) peer.DeviceName = device res := m.db.Create(&peer) @@ -361,6 +471,14 @@ func (m *PeerManager) validateOrCreatePeer(device string, wgPeer wgtypes.Peer) e } } + if peer.DeviceName == "" { + peer.DeviceName = device + res := m.db.Save(&peer) + if res.Error != nil { + return errors.Wrapf(res.Error, "failed to update autodetected peer %s", peer.PublicKey) + } + } + return nil } @@ -370,12 +488,14 @@ func (m *PeerManager) validateOrCreateDevice(dev wgtypes.Device, ipAddresses []s m.db.Where("device_name = ?", dev.Name).FirstOrInit(&device) if device.PublicKey == "" { // device not found, create + device.Type = DeviceTypeServer // imported device, we assume that server mode is used device.PublicKey = dev.PublicKey.String() device.PrivateKey = dev.PrivateKey.String() device.DeviceName = dev.Name device.ListenPort = dev.ListenPort + device.FirewallMark = int32(dev.FirewallMark) device.Mtu = 0 - device.PersistentKeepalive = 16 // Default + device.DefaultPersistentKeepalive = 16 // Default device.IPsStr = strings.Join(ipAddresses, ", ") if mtu == DefaultMTU { mtu = 0 @@ -388,13 +508,20 @@ func (m *PeerManager) validateOrCreateDevice(dev wgtypes.Device, ipAddresses []s } } + if device.Type == "" { + device.Type = DeviceTypeServer // from version <= 1.0.3, only server mode devices were supported + + res := m.db.Save(&device) + if res.Error != nil { + return errors.Wrapf(res.Error, "failed to update autodetected device") + } + } + return nil } // populatePeerData enriches the peer struct with WireGuard live data like last handshake, ... func (m *PeerManager) populatePeerData(peer *Peer) { - peer.AllowedIPs = strings.Split(peer.AllowedIPsStr, ", ") - peer.IPs = strings.Split(peer.IPsStr, ", ") // Set config file tmpCfg, _ := peer.GetConfigFile(m.GetDevice(peer.DeviceName)) peer.Config = string(tmpCfg) @@ -421,12 +548,28 @@ func (m *PeerManager) populatePeerData(peer *Peer) { peer.IsOnline = false } +// fixPeerDefaultData tries to fill all required fields for the given peer +// also tries to migrate data if the database schema changed +func (m *PeerManager) fixPeerDefaultData(peer *Peer, device *Device) error { + updatePeer := false + + switch device.Type { + case DeviceTypeServer: + if peer.Endpoint == "" { + peer.Endpoint = device.DefaultEndpoint + updatePeer = true + } + case DeviceTypeClient: + } + + if updatePeer { + return m.UpdatePeer(*peer) + } + return nil +} + // populateDeviceData enriches the device struct with WireGuard live data like interface information func (m *PeerManager) populateDeviceData(device *Device) { - device.AllowedIPs = strings.Split(device.AllowedIPsStr, ", ") - device.IPs = strings.Split(device.IPsStr, ", ") - device.DNS = strings.Split(device.DNSStr, ", ") - // set data from WireGuard interface device.Interface, _ = m.wg.GetDeviceInfo(device.DeviceName) } @@ -469,39 +612,7 @@ func (m *PeerManager) GetFilteredAndSortedPeers(device, sortKey, sortDirection, } } - sort.Slice(filteredPeers, func(i, j int) bool { - var sortValueLeft string - var sortValueRight string - - switch sortKey { - case "id": - sortValueLeft = filteredPeers[i].Identifier - sortValueRight = filteredPeers[j].Identifier - case "pubKey": - sortValueLeft = filteredPeers[i].PublicKey - sortValueRight = filteredPeers[j].PublicKey - case "mail": - sortValueLeft = filteredPeers[i].Email - sortValueRight = filteredPeers[j].Email - case "ip": - sortValueLeft = filteredPeers[i].IPsStr - sortValueRight = filteredPeers[j].IPsStr - case "handshake": - if filteredPeers[i].Peer == nil { - return false - } else if filteredPeers[j].Peer == nil { - return true - } - sortValueLeft = filteredPeers[i].Peer.LastHandshakeTime.Format(time.RFC3339) - sortValueRight = filteredPeers[j].Peer.LastHandshakeTime.Format(time.RFC3339) - } - - if sortDirection == "asc" { - return sortValueLeft < sortValueRight - } else { - return sortValueLeft > sortValueRight - } - }) + sortPeers(sortKey, sortDirection, filteredPeers) return filteredPeers } @@ -514,6 +625,12 @@ func (m *PeerManager) GetSortedPeersForEmail(sortKey, sortDirection, email strin m.populatePeerData(&peers[i]) } + sortPeers(sortKey, sortDirection, peers) + + return peers +} + +func sortPeers(sortKey string, sortDirection string, peers []Peer) { sort.Slice(peers, func(i, j int) bool { var sortValueLeft string var sortValueRight string @@ -531,6 +648,9 @@ func (m *PeerManager) GetSortedPeersForEmail(sortKey, sortDirection, email strin case "ip": sortValueLeft = peers[i].IPsStr sortValueRight = peers[j].IPsStr + case "endpoint": + sortValueLeft = peers[i].Endpoint + sortValueRight = peers[j].Endpoint case "handshake": if peers[i].Peer == nil { return true @@ -547,8 +667,6 @@ func (m *PeerManager) GetSortedPeersForEmail(sortKey, sortDirection, email strin return sortValueLeft > sortValueRight } }) - - return peers } func (m *PeerManager) GetDevice(device string) Device { @@ -583,8 +701,6 @@ func (m *PeerManager) CreatePeer(peer Peer) error { peer.UID = fmt.Sprintf("u%x", md5.Sum([]byte(peer.PublicKey))) peer.UpdatedAt = time.Now() peer.CreatedAt = time.Now() - peer.AllowedIPsStr = strings.Join(peer.AllowedIPs, ", ") - peer.IPsStr = strings.Join(peer.IPs, ", ") res := m.db.Create(&peer) if res.Error != nil { @@ -597,8 +713,6 @@ func (m *PeerManager) CreatePeer(peer Peer) error { func (m *PeerManager) UpdatePeer(peer Peer) error { peer.UpdatedAt = time.Now() - peer.AllowedIPsStr = strings.Join(peer.AllowedIPs, ", ") - peer.IPsStr = strings.Join(peer.IPs, ", ") res := m.db.Save(&peer) if res.Error != nil { @@ -621,9 +735,6 @@ func (m *PeerManager) DeletePeer(peer Peer) error { func (m *PeerManager) UpdateDevice(device Device) error { device.UpdatedAt = time.Now() - device.AllowedIPsStr = strings.Join(device.AllowedIPs, ", ") - device.IPsStr = strings.Join(device.IPs, ", ") - device.DNSStr = strings.Join(device.DNS, ", ") res := m.db.Save(&device) if res.Error != nil { @@ -640,7 +751,7 @@ func (m *PeerManager) GetAllReservedIps(device string) ([]string, error) { reservedIps := make([]string, 0) peers := m.GetAllPeers(device) for _, user := range peers { - for _, cidr := range user.IPs { + for _, cidr := range user.GetIPAddresses() { if cidr == "" { continue } @@ -653,7 +764,7 @@ func (m *PeerManager) GetAllReservedIps(device string) ([]string, error) { } dev := m.GetDevice(device) - for _, cidr := range dev.IPs { + for _, cidr := range dev.GetIPAddresses() { if cidr == "" { continue } diff --git a/internal/wireguard/template.go b/internal/wireguard/template.go index 3b65e57..d9c2e4a 100644 --- a/internal/wireguard/template.go +++ b/internal/wireguard/template.go @@ -1,53 +1,20 @@ package wireguard -var ( - ClientCfgTpl = `#{{ .Client.Identifier }} -[Interface] -Address = {{ .Client.IPsStr }} -PrivateKey = {{ .Client.PrivateKey }} -{{- if .Server.DNSStr}} -DNS = {{ .Server.DNSStr }} -{{- end}} -{{- if ne .Server.Mtu 0}} -MTU = {{.Server.Mtu}} -{{- end}} - -[Peer] -PublicKey = {{ .Server.PublicKey }} -{{- if .Client.PresharedKey}} -PresharedKey = {{ .Client.PresharedKey }} -{{- end}} -AllowedIPs = {{ .Client.AllowedIPsStr }} -Endpoint = {{ .Server.Endpoint }} -{{- if and (ne .Server.PersistentKeepalive 0) (not .Client.IgnorePersistentKeepalive)}} -PersistentKeepalive = {{.Server.PersistentKeepalive}} -{{- end}} -` - DeviceCfgTpl = `# AUTOGENERATED FILE - DO NOT EDIT -# Updated: {{ .Server.UpdatedAt }} / Created: {{ .Server.CreatedAt }} -[Interface] -{{- range .Server.IPs}} -Address = {{ . }} -{{- end}} -ListenPort = {{ .Server.ListenPort }} -PrivateKey = {{ .Server.PrivateKey }} -{{- if ne .Server.Mtu 0}} -MTU = {{.Server.Mtu}} -{{- end}} -PreUp = {{ .Server.PreUp }} -PostUp = {{ .Server.PostUp }} -PreDown = {{ .Server.PreDown }} -PostDown = {{ .Server.PostDown }} - -{{range .Clients}} -{{if not .DeactivatedAt -}} -# {{.Identifier}} / {{.Email}} / Updated: {{.UpdatedAt}} / Created: {{.CreatedAt}} -[Peer] -PublicKey = {{ .PublicKey }} -{{- if .PresharedKey}} -PresharedKey = {{ .PresharedKey }} -{{- end}} -AllowedIPs = {{ StringsJoin .IPs ", " }} -{{- end}} -{{end}}` +import ( + "embed" + "strings" + "text/template" ) + +//go:embed tpl/* +var Templates embed.FS + +var templateCache *template.Template + +func init() { + var err error + templateCache, err = template.New("server").Funcs(template.FuncMap{"StringsJoin": strings.Join}).ParseFS(Templates, "tpl/*.tpl") + if err != nil { + panic(err) + } +} diff --git a/internal/wireguard/tpl/interface.tpl b/internal/wireguard/tpl/interface.tpl new file mode 100644 index 0000000..318e2b7 --- /dev/null +++ b/internal/wireguard/tpl/interface.tpl @@ -0,0 +1,78 @@ +# AUTOGENERATED FILE - DO NOT EDIT +# -WGP- Interface: {{ .Interface.DeviceName }} / Updated: {{ .Interface.UpdatedAt }} / Created: {{ .Interface.CreatedAt }} +# -WGP- Interface display name: {{ .Interface.DisplayName }} +# -WGP- Interface mode: {{ .Interface.Type }} +# -WGP- PublicKey = {{ .Interface.PublicKey }} + +[Interface] + +# Core settings +PrivateKey = {{ .Interface.PrivateKey }} +Address = {{ .Interface.IPsStr }} + +# 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 .DeactivatedAt}} +# -WGP- Peer: {{.Identifier}} / Updated: {{.UpdatedAt}} / Created: {{.CreatedAt}} +# -WGP- Peer email: {{.Email}} +{{- if .PrivateKey}} +# -WGP- PrivateKey: {{.PrivateKey}} +{{- end}} +[Peer] +PublicKey = {{ .PublicKey }} +{{- if .PresharedKey}} +PresharedKey = {{ .PresharedKey }} +{{- end}} +{{- if eq $.Interface.Type "server"}} +AllowedIPs = {{ .IPsStr }} +{{- end}} +{{- if eq $.Interface.Type "client"}} +{{- if .AllowedIPsStr}} +AllowedIPs = {{ .AllowedIPsStr }} +{{- end}} +{{- end}} +{{- if and (ne .Endpoint "") (eq $.Interface.Type "client")}} +Endpoint = {{ .Endpoint }} +{{- end}} +{{- if ne .PersistentKeepalive 0}} +PersistentKeepalive = {{ .PersistentKeepalive }} +{{- end}} +{{- end}} +{{end}} \ No newline at end of file diff --git a/internal/wireguard/tpl/peer.tpl b/internal/wireguard/tpl/peer.tpl new file mode 100644 index 0000000..d6a85e4 --- /dev/null +++ b/internal/wireguard/tpl/peer.tpl @@ -0,0 +1,30 @@ +# AUTOGENERATED FILE - PROVIDED BY WIREGUARD PORTAL +# WireGuard configuration: {{ .Peer.Identifier }} +# -WGP- PublicKey: {{ .Peer.PublicKey }} + +[Interface] + +# Core settings +PrivateKey = {{ .Peer.PrivateKey }} +Address = {{ .Peer.IPsStr }} + +# Misc. settings (optional) +{{- if .Peer.DNSStr}} +DNS = {{ .Peer.DNSStr }} +{{- end}} +{{- if ne .Peer.Mtu 0}} +MTU = {{.Peer.Mtu}} +{{- end}} + +[Peer] +PublicKey = {{ .Interface.PublicKey }} +Endpoint = {{ .Peer.Endpoint }} +{{- if .Peer.AllowedIPsStr}} +AllowedIPs = {{ .Peer.AllowedIPsStr }} +{{- end}} +{{- if .Peer.PresharedKey}} +PresharedKey = {{ .Peer.PresharedKey }} +{{- end}} +{{- if ne .Peer.PersistentKeepalive 0}} +PersistentKeepalive = {{.Peer.PersistentKeepalive}} +{{- end}} \ No newline at end of file