diff --git a/Dockerfile b/Dockerfile index c4e3b6a98e31238f9eab6256e5dc96958e25c703..ef1f911faef46c619123cb2e003d1c97d2a23b91 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ WORKDIR /app ADD . . RUN go build -o wg-gen-web-linux -FROM node:10-alpine as build-front +FROM node:10-alpine AS build-front WORKDIR /app ADD ui . RUN npm install diff --git a/core/client.go b/core/client.go index 8d49c703c0fc5c465fc3641931d8fb97f2bff07c..e2839fcd5ead7318185e2a349681cff44d00acdd 100644 --- a/core/client.go +++ b/core/client.go @@ -22,6 +22,17 @@ import ( // CreateClient client with all necessary data func CreateClient(client *model.Client) (*model.Client, error) { + // check if client is valid + errs := client.IsValid() + if len(errs) != 0 { + for _, err := range errs { + log.WithFields(log.Fields{ + "err": err, + }).Error("client validation error") + } + return nil, errors.New("failed to validate client") + } + u := uuid.NewV4() client.Id = u.String() @@ -32,32 +43,14 @@ func CreateClient(client *model.Client) (*model.Client, error) { client.PrivateKey = key.String() client.PublicKey = key.PublicKey().String() - // find available IP address from selected networks - clients, err := ReadClients() + reserverIps, err := GetAllReservedIps() if err != nil { return nil, err } - reserverIps := make([]string, 0) - for _, client := range clients { - ips := strings.Split(client.Address, ",") - for i := range ips { - if util.IsIPv6(ips[i]) { - ips[i] = strings.ReplaceAll(strings.TrimSpace(ips[i]), "/128", "") - } else { - ips[i] = strings.ReplaceAll(strings.TrimSpace(ips[i]), "/32", "") - } - } - reserverIps = append(reserverIps, ips...) - } - - networks := strings.Split(client.Address, ",") - for i := range networks { - networks[i] = strings.TrimSpace(networks[i]) - } ips := make([]string, 0) - for _, network := range networks { - ip, err := util.GetAvailableIp(network, reserverIps) + for _, network := range strings.Split(client.Address, ",") { + ip, err := util.GetAvailableIp(strings.TrimSpace(network), reserverIps) if err != nil { return nil, err } @@ -109,6 +102,18 @@ func UpdateClient(Id string, client *model.Client) (*model.Client, error) { if current.Id != client.Id { return nil, errors.New("records Id mismatch") } + + // check if client is valid + errs := client.IsValid() + if len(errs) != 0 { + for _, err := range errs { + log.WithFields(log.Fields{ + "err": err, + }).Error("client validation error") + } + return nil, errors.New("failed to validate client") + } + // keep keys client.PrivateKey = current.PrivateKey client.PublicKey = current.PublicKey @@ -159,7 +164,7 @@ func ReadClients() ([]*model.Client, error) { log.WithFields(log.Fields{ "err": err, "path": f.Name(), - }).Error("failed to storage.Destorage.Serialize client") + }).Error("failed to deserialize client") } else { clients = append(clients, c.(*model.Client)) } diff --git a/core/server.go b/core/server.go index 5069498cc34a3d27f6d9e7ea609c9356ad6c48dc..a4535d794927d85d2e76dab6089e45bc2cca0690 100644 --- a/core/server.go +++ b/core/server.go @@ -1,6 +1,8 @@ package core import ( + "errors" + log "github.com/sirupsen/logrus" "gitlab.127-0-0-1.fr/vx3r/wg-gen-web/model" "gitlab.127-0-0-1.fr/vx3r/wg-gen-web/storage" "gitlab.127-0-0-1.fr/vx3r/wg-gen-web/template" @@ -8,6 +10,7 @@ import ( "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "os" "path/filepath" + "strings" "time" ) @@ -62,6 +65,18 @@ func UpdateServer(server *model.Server) (*model.Server, error) { if err != nil { return nil, err } + + // check if server is valid + errs := server.IsValid() + if len(errs) != 0 { + for _, err := range errs { + log.WithFields(log.Fields{ + "err": err, + }).Error("server validation error") + } + return nil, errors.New("failed to validate server") + } + server.PrivateKey = current.(*model.Server).PrivateKey server.PublicKey = current.(*model.Server).PublicKey server.PresharedKey = current.(*model.Server).PresharedKey @@ -100,3 +115,46 @@ func UpdateServerConfigWg() error { return nil } + +// GetAllReservedIps the list of all reserved IPs, client and server +func GetAllReservedIps() ([]string, error) { + clients, err := ReadClients() + if err != nil { + return nil, err + } + + server, err := ReadServer() + if err != nil { + return nil, err + } + + reserverIps := make([]string, 0) + + for _, client := range clients { + for _, cidr := range strings.Split(client.Address, ",") { + ip, err := util.GetIpFromCidr(strings.TrimSpace(cidr)) + if err != nil { + log.WithFields(log.Fields{ + "err": err, + "cidr": err, + }).Error("failed to ip from cidr") + } else { + reserverIps = append(reserverIps, ip) + } + } + } + + for _, cidr := range strings.Split(server.Address, ",") { + ip, err := util.GetIpFromCidr(strings.TrimSpace(cidr)) + if err != nil { + log.WithFields(log.Fields{ + "err": err, + "cidr": err, + }).Error("failed to ip from cidr") + } else { + reserverIps = append(reserverIps, ip) + } + } + + return reserverIps, nil +} diff --git a/model/client.go b/model/client.go index 7b182c32ff1f1e4daa8113cd5c02e458b347208b..3ff5dcba2e0f83020c0bea28fa804592ae60041c 100644 --- a/model/client.go +++ b/model/client.go @@ -1,6 +1,11 @@ package model -import "time" +import ( + "fmt" + "gitlab.127-0-0-1.fr/vx3r/wg-gen-web/util" + "strings" + "time" +) // Client structure type Client struct { @@ -8,10 +13,53 @@ type Client struct { Name string `json:"name"` Email string `json:"email"` Enable bool `json:"enable"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` AllowedIPs string `json:"allowedIPs"` Address string `json:"address"` PrivateKey string `json:"privateKey"` PublicKey string `json:"publicKey"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` +} + +func (a Client) IsValid() []error { + errs := make([]error, 0) + + // check if the name empty + if a.Name == "" { + errs = append(errs, fmt.Errorf("name is required")) + } + // check the name field is between 3 to 40 chars + if len(a.Name) < 2 || len(a.Name) > 40 { + errs = append(errs, fmt.Errorf("name field must be between 2-40 chars")) + } + // check if the email empty + if a.Email == "" { + errs = append(errs, fmt.Errorf("email field is required")) + } + // check if email valid + if !util.RegexpEmail.MatchString(a.Email) { + errs = append(errs, fmt.Errorf("email %s is invalid", a.Email)) + } + // check if the allowedIPs empty + if a.AllowedIPs == "" { + errs = append(errs, fmt.Errorf("allowedIPs field is required")) + } + // check if the allowedIPs are valid + for _, allowedIP := range strings.Split(a.AllowedIPs, ",") { + if !util.IsValidCidr(strings.TrimSpace(allowedIP)) { + errs = append(errs, fmt.Errorf("allowedIP %s is invalid", allowedIP)) + } + } + // check if the address empty + if a.Address == "" { + errs = append(errs, fmt.Errorf("address field is required")) + } + // check if the address are valid + for _, address := range strings.Split(a.Address, ",") { + if !util.IsValidCidr(strings.TrimSpace(address)) { + errs = append(errs, fmt.Errorf("address %s is invalid", address)) + } + } + + return errs } diff --git a/model/server.go b/model/server.go index e27df514ab1633a13157b52598480224f1ffc4bc..cb52ce02116d6289c441274152345fe93caf3ac7 100644 --- a/model/server.go +++ b/model/server.go @@ -1,12 +1,15 @@ package model -import "time" +import ( + "fmt" + "gitlab.127-0-0-1.fr/vx3r/wg-gen-web/util" + "strings" + "time" +) // Server structure type Server struct { Name string `json:"name"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` Address string `json:"address"` ListenPort int `json:"listenPort"` PrivateKey string `json:"privateKey"` @@ -19,4 +22,53 @@ type Server struct { PostUp string `json:"postUp"` PreDown string `json:"preDown"` PostDown string `json:"postDown"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` +} + +func (a Server) IsValid() []error { + errs := make([]error, 0) + + // check if the name empty + if a.Name == "" { + errs = append(errs, fmt.Errorf("name is required")) + } + // check the name field is between 3 to 40 chars + if len(a.Name) < 2 || len(a.Name) > 40 { + errs = append(errs, fmt.Errorf("name must be between 2-40 chars")) + } + // check if the address empty + if a.Address == "" { + errs = append(errs, fmt.Errorf("address is required")) + } + // check if the address are valid + for _, address := range strings.Split(a.Address, ",") { + if !util.IsValidCidr(strings.TrimSpace(address)) { + errs = append(errs, fmt.Errorf("address %s is invalid", address)) + } + } + // check if the listenPort is valid + if a.ListenPort < 0 || a.ListenPort > 65535 { + errs = append(errs, fmt.Errorf("listenPort %s is invalid", a.ListenPort)) + } + // check if the endpoint empty + if a.Endpoint == "" { + errs = append(errs, fmt.Errorf("endpoint is required")) + } + // check if the persistentKeepalive is valid + if a.PersistentKeepalive < 0 { + errs = append(errs, fmt.Errorf("persistentKeepalive %d is invalid", a.PersistentKeepalive)) + } + // check if the dns empty + if a.Dns == "" { + errs = append(errs, fmt.Errorf("dns is required")) + } + // check if the address are valid + for _, dns := range strings.Split(a.Dns, ",") { + if !util.IsValidIp(strings.TrimSpace(dns)) { + errs = append(errs, fmt.Errorf("dns %s is invalid", dns)) + } + } + + return errs } diff --git a/ui/src/App.vue b/ui/src/App.vue index 753e268f9cad1bb20fcd7621653f77e762635298..e36928987b7c31b592ee07aeb10e309af1d7abdf 100644 --- a/ui/src/App.vue +++ b/ui/src/App.vue @@ -13,7 +13,7 @@ </v-content> <v-footer app> - <span>Copyright <a class="pr-1 pl-1" href="http://www.wtfpl.net/" target="_blank">WTFPL</a> © {{ new Date().getFullYear() }} Created with</span><v-icon class="pr-1 pl-1">mdi-heart</v-icon><span>by</span><a class="pr-1 pl-1" href="mailto:vx3r@127-0-0-1.fr">vx3r</a> + <span>License <a class="pr-1 pl-1" href="http://www.wtfpl.net/" target="_blank">WTFPL</a> © {{ new Date().getFullYear() }} Created with</span><v-icon class="pr-1 pl-1">mdi-heart</v-icon><span>by</span><a class="pr-1 pl-1" href="mailto:vx3r@127-0-0-1.fr">vx3r</a> </v-footer> </v-app> diff --git a/ui/src/views/Home.vue b/ui/src/views/Home.vue index 1968b0d55afee8a99feb157cdc94cac417f66697..7a6d2db3c8e4c819d054c5d49cb7edcbcde226c5 100644 --- a/ui/src/views/Home.vue +++ b/ui/src/views/Home.vue @@ -318,12 +318,14 @@ @click="addClient(client)" > Submit + <v-icon right dark>mdi-check-outline</v-icon> </v-btn> <v-btn color="primary" @click="dialogAddClient = false" > Cancel + <v-icon right dark>mdi-close-circle-outline</v-icon> </v-btn> </v-card-actions> </v-card> @@ -361,6 +363,26 @@ ]" required /> + <v-combobox + v-model="client.address" + chips + hint="Write IPv4 or IPv6 CIDR and hit enter" + label="Addresses" + multiple + dark + > + <template v-slot:selection="{ attrs, item, select, selected }"> + <v-chip + v-bind="attrs" + :input-value="selected" + close + @click="select" + @click:close="client.address.splice(client.address.indexOf(item), 1)" + > + <strong>{{ item }}</strong> + </v-chip> + </template> + </v-combobox> <v-combobox v-model="client.allowedIPs" chips @@ -393,12 +415,14 @@ @click="updateClient(client)" > Submit + <v-icon right dark>mdi-check-outline</v-icon> </v-btn> <v-btn color="primary" @click="dialogEditClient = false" > Cancel + <v-icon right dark>mdi-close-circle-outline</v-icon> </v-btn> </v-card-actions> </v-card> @@ -460,13 +484,19 @@ this.$get(`/client/${id}`).then((res) => { this.dialogEditClient = true; res.allowedIPs = res.allowedIPs.split(','); + res.address = res.address.split(','); this.client = res }).catch((e) => { this.notify('error', e.response.status + ' ' + e.response.statusText); }); }, disableClient(client) { - client.allowedIPs = client.allowedIPs.split(','); + if(!Array.isArray(client.allowedIPs)){ + client.allowedIPs = client.allowedIPs.split(','); + } + if(!Array.isArray(client.address)){ + client.address = client.address.split(','); + } this.updateClient(client) }, getData() { @@ -476,7 +506,6 @@ this.server = res; this.clientAddress = this.serverAddress = this.server.address }).catch((e) => { - console.log(e) this.notify('error', e.response.status + ' ' + e.response.statusText); }); @@ -564,6 +593,7 @@ } }, updateClient(client) { + // check allowed IPs if (client.allowedIPs.length < 1) { this.notify('error', 'Please provide at least one valid CIDR address for client allowed IPs'); return; @@ -574,9 +604,21 @@ return } } - + // check address + if (client.address.length < 1) { + this.notify('error', 'Please provide at least one valid CIDR address for client'); + return; + } + for (let i = 0; i < client.address.length; i++){ + if (this.$isCidr(client.address[i]) === 0) { + this.notify('error', 'Invalid CIDR detected, please correct before submitting'); + return + } + } + // all good, submit this.dialogEditClient = false; client.allowedIPs = client.allowedIPs.join(','); + client.address = client.address.join(','); this.$patch(`/client/${client.id}`, client).then((res) => { this.notify('success', "Client successfully updated"); diff --git a/util/util.go b/util/util.go index cced1a9b01ec72b374b6edff1e68c77d72122a84..403f82e4bb436173aec13b5bb6f39a9e8342ddbd 100644 --- a/util/util.go +++ b/util/util.go @@ -5,7 +5,11 @@ import ( "io/ioutil" "net" "os" - "strings" + "regexp" +) + +var ( + RegexpEmail = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") ) // ReadFile file content @@ -86,7 +90,31 @@ func GetAllAddressesFromCidr(cidr string) ([]string, error) { // IsIPv6 check if given ip is IPv6 func IsIPv6(address string) bool { - return strings.Count(address, ":") >= 2 + ip := net.ParseIP(address) + if ip == nil { + return false + } + return ip.To4() == nil +} + +// IsValidIp check if ip is valid +func IsValidIp(ip string) bool { + return net.ParseIP(ip) != nil +} + +// IsValidCidr check if CIDR is valid +func IsValidCidr(cidr string) bool { + _, _, err := net.ParseCIDR(cidr) + return err == nil +} + +// GetIpFromCidr get ip from cidr +func GetIpFromCidr(cidr string) (string, error) { + ip, _, err := net.ParseCIDR(cidr) + if err != nil { + return "", err + } + return ip.String(), nil } // http://play.golang.org/p/m8TNTtygK0