diff --git a/controllers/network.go b/controllers/network.go index 5b72bb3e..695d5b72 100644 --- a/controllers/network.go +++ b/controllers/network.go @@ -559,7 +559,7 @@ func createNetwork(w http.ResponseWriter, r *http.Request) { logic.CreateDefaultNetworkRolesAndGroups(models.NetworkID(network.NetID)) logic.CreateDefaultAclNetworkPolicies(models.NetworkID(network.NetID)) logic.CreateDefaultTags(models.NetworkID(network.NetID)) - //add new network to allocated ip map + go logic.AddNetworkToAllocatedIpMap(network.NetID) go func() { diff --git a/logic/acls.go b/logic/acls.go index 1b0aebfa..3a3391cf 100644 --- a/logic/acls.go +++ b/logic/acls.go @@ -17,7 +17,6 @@ import ( var ( aclCacheMutex = &sync.RWMutex{} aclCacheMap = make(map[string]models.Acl) - aclTagsMutex = &sync.RWMutex{} ) func MigrateAclPolicies() { @@ -577,10 +576,22 @@ func IsPeerAllowed(node, peer models.Node, checkDefaultPolicy bool) bool { if peer.IsStatic { peer = peer.StaticNode.ConvertToStaticNode() } - aclTagsMutex.RLock() - peerTags := maps.Clone(peer.Tags) - nodeTags := maps.Clone(node.Tags) - aclTagsMutex.RUnlock() + var nodeTags, peerTags map[models.TagID]struct{} + if node.Mutex != nil { + node.Mutex.Lock() + nodeTags = maps.Clone(node.Tags) + node.Mutex.Unlock() + } else { + nodeTags = node.Tags + } + if peer.Mutex != nil { + peer.Mutex.Lock() + peerTags = maps.Clone(peer.Tags) + peer.Mutex.Unlock() + } else { + peerTags = peer.Tags + } + if checkDefaultPolicy { // check default policy if all allowed return true defaultPolicy, err := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy) @@ -663,10 +674,21 @@ func IsNodeAllowedToCommunicate(node, peer models.Node, checkDefaultPolicy bool) if peer.IsStatic { peer = peer.StaticNode.ConvertToStaticNode() } - aclTagsMutex.RLock() - peerTags := maps.Clone(peer.Tags) - nodeTags := maps.Clone(node.Tags) - aclTagsMutex.RUnlock() + var nodeTags, peerTags map[models.TagID]struct{} + if node.Mutex != nil { + node.Mutex.Lock() + nodeTags = maps.Clone(node.Tags) + node.Mutex.Unlock() + } else { + nodeTags = node.Tags + } + if peer.Mutex != nil { + peer.Mutex.Lock() + peerTags = maps.Clone(peer.Tags) + peer.Mutex.Unlock() + } else { + peerTags = peer.Tags + } if checkDefaultPolicy { // check default policy if all allowed return true defaultPolicy, err := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy) @@ -864,7 +886,15 @@ func getUserAclRulesForNode(targetnode *models.Node, userGrpMap := GetUserGrpMap() allowedUsers := make(map[string][]models.Acl) acls := listUserPolicies(models.NetworkID(targetnode.Network)) - for nodeTag := range targetnode.Tags { + var targetNodeTags = make(map[models.TagID]struct{}) + if targetnode.Mutex != nil { + targetnode.Mutex.Lock() + targetNodeTags = maps.Clone(targetnode.Tags) + targetnode.Mutex.Unlock() + } else { + targetNodeTags = maps.Clone(targetnode.Tags) + } + for nodeTag := range targetNodeTags { for _, acl := range acls { if !acl.Enabled { continue @@ -888,6 +918,7 @@ func getUserAclRulesForNode(targetnode *models.Node, } } } + for _, userNode := range userNodes { if !userNode.StaticNode.Enabled { continue @@ -944,8 +975,17 @@ func GetAclRulesForNode(targetnode *models.Node) (rules map[string]models.AclRul } acls := listDevicePolicies(models.NetworkID(targetnode.Network)) - targetnode.Tags["*"] = struct{}{} - for nodeTag := range targetnode.Tags { + + var targetNodeTags = make(map[models.TagID]struct{}) + if targetnode.Mutex != nil { + targetnode.Mutex.Lock() + targetNodeTags = maps.Clone(targetnode.Tags) + targetnode.Mutex.Unlock() + } else { + targetNodeTags = maps.Clone(targetnode.Tags) + } + targetNodeTags["*"] = struct{}{} + for nodeTag := range targetNodeTags { for _, acl := range acls { if !acl.Enabled { continue diff --git a/logic/extpeers.go b/logic/extpeers.go index 706c5631..89934e56 100644 --- a/logic/extpeers.go +++ b/logic/extpeers.go @@ -28,6 +28,9 @@ var ( func getAllExtClientsFromCache() (extClients []models.ExtClient) { extClientCacheMutex.RLock() for _, extclient := range extClientCacheMap { + if extclient.Mutex == nil { + extclient.Mutex = &sync.Mutex{} + } extClients = append(extClients, extclient) } extClientCacheMutex.RUnlock() @@ -43,12 +46,18 @@ func deleteExtClientFromCache(key string) { func getExtClientFromCache(key string) (extclient models.ExtClient, ok bool) { extClientCacheMutex.RLock() extclient, ok = extClientCacheMap[key] + if extclient.Mutex == nil { + extclient.Mutex = &sync.Mutex{} + } extClientCacheMutex.RUnlock() return } func storeExtClientInCache(key string, extclient models.ExtClient) { extClientCacheMutex.Lock() + if extclient.Mutex == nil { + extclient.Mutex = &sync.Mutex{} + } extClientCacheMap[key] = extclient extClientCacheMutex.Unlock() } @@ -96,14 +105,14 @@ func DeleteExtClient(network string, clientid string) error { if err != nil { return err } - //recycle ip address - if extClient.Address != "" { - RemoveIpFromAllocatedIpMap(network, extClient.Address) - } - if extClient.Address6 != "" { - RemoveIpFromAllocatedIpMap(network, extClient.Address6) - } if servercfg.CacheEnabled() { + // recycle ip address + if extClient.Address != "" { + RemoveIpFromAllocatedIpMap(network, extClient.Address) + } + if extClient.Address6 != "" { + RemoveIpFromAllocatedIpMap(network, extClient.Address6) + } deleteExtClientFromCache(key) } return nil @@ -333,15 +342,16 @@ func SaveExtClient(extclient *models.ExtClient) error { } if servercfg.CacheEnabled() { storeExtClientInCache(key, *extclient) - } - if _, ok := allocatedIpMap[extclient.Network]; ok { - if extclient.Address != "" { - AddIpToAllocatedIpMap(extclient.Network, net.ParseIP(extclient.Address)) - } - if extclient.Address6 != "" { - AddIpToAllocatedIpMap(extclient.Network, net.ParseIP(extclient.Address6)) + if _, ok := allocatedIpMap[extclient.Network]; ok { + if extclient.Address != "" { + AddIpToAllocatedIpMap(extclient.Network, net.ParseIP(extclient.Address)) + } + if extclient.Address6 != "" { + AddIpToAllocatedIpMap(extclient.Network, net.ParseIP(extclient.Address6)) + } } } + return SetNetworkNodesLastModified(extclient.Network) } diff --git a/logic/networks.go b/logic/networks.go index 04b6b573..4e55946a 100644 --- a/logic/networks.go +++ b/logic/networks.go @@ -30,6 +30,9 @@ var ( // SetAllocatedIpMap - set allocated ip map for networks func SetAllocatedIpMap() error { + if !servercfg.CacheEnabled() { + return nil + } logger.Log(0, "start setting up allocated ip map") if allocatedIpMap == nil { allocatedIpMap = map[string]map[string]net.IP{} @@ -84,16 +87,25 @@ func SetAllocatedIpMap() error { // ClearAllocatedIpMap - set allocatedIpMap to nil func ClearAllocatedIpMap() { + if !servercfg.CacheEnabled() { + return + } allocatedIpMap = nil } func AddIpToAllocatedIpMap(networkName string, ip net.IP) { + if !servercfg.CacheEnabled() { + return + } networkCacheMutex.Lock() allocatedIpMap[networkName][ip.String()] = ip networkCacheMutex.Unlock() } func RemoveIpFromAllocatedIpMap(networkName string, ip string) { + if !servercfg.CacheEnabled() { + return + } networkCacheMutex.Lock() delete(allocatedIpMap[networkName], ip) networkCacheMutex.Unlock() @@ -101,6 +113,10 @@ func RemoveIpFromAllocatedIpMap(networkName string, ip string) { // AddNetworkToAllocatedIpMap - add network to allocated ip map when network is added func AddNetworkToAllocatedIpMap(networkName string) { + //add new network to allocated ip map + if !servercfg.CacheEnabled() { + return + } networkCacheMutex.Lock() allocatedIpMap[networkName] = make(map[string]net.IP) networkCacheMutex.Unlock() @@ -108,6 +124,9 @@ func AddNetworkToAllocatedIpMap(networkName string) { // RemoveNetworkFromAllocatedIpMap - remove network from allocated ip map when network is deleted func RemoveNetworkFromAllocatedIpMap(networkName string) { + if !servercfg.CacheEnabled() { + return + } networkCacheMutex.Lock() delete(allocatedIpMap, networkName) networkCacheMutex.Unlock() @@ -354,7 +373,7 @@ func GetNetworkSettings(networkname string) (models.Network, error) { } // UniqueAddress - get a unique ipv4 address -func UniqueAddress(networkName string, reverse bool) (net.IP, error) { +func UniqueAddressCache(networkName string, reverse bool) (net.IP, error) { add := net.IP{} var network models.Network network, err := GetParentNetwork(networkName) @@ -396,6 +415,49 @@ func UniqueAddress(networkName string, reverse bool) (net.IP, error) { return add, errors.New("ERROR: No unique addresses available. Check network subnet") } +// UniqueAddress - get a unique ipv4 address +func UniqueAddressDB(networkName string, reverse bool) (net.IP, error) { + add := net.IP{} + var network models.Network + network, err := GetParentNetwork(networkName) + if err != nil { + logger.Log(0, "UniqueAddressServer encountered an error") + return add, err + } + + if network.IsIPv4 == "no" { + return add, fmt.Errorf("IPv4 not active on network " + networkName) + } + //ensure AddressRange is valid + if _, _, err := net.ParseCIDR(network.AddressRange); err != nil { + logger.Log(0, "UniqueAddress encountered an error") + return add, err + } + net4 := iplib.Net4FromStr(network.AddressRange) + newAddrs := net4.FirstAddress() + + if reverse { + newAddrs = net4.LastAddress() + } + + for { + if IsIPUnique(networkName, newAddrs.String(), database.NODES_TABLE_NAME, false) && + IsIPUnique(networkName, newAddrs.String(), database.EXT_CLIENT_TABLE_NAME, false) { + return newAddrs, nil + } + if reverse { + newAddrs, err = net4.PreviousIP(newAddrs) + } else { + newAddrs, err = net4.NextIP(newAddrs) + } + if err != nil { + break + } + } + + return add, errors.New("ERROR: No unique addresses available. Check network subnet") +} + // IsIPUnique - checks if an IP is unique func IsIPUnique(network string, ip string, tableName string, isIpv6 bool) bool { @@ -439,9 +501,67 @@ func IsIPUnique(network string, ip string, tableName string, isIpv6 bool) bool { return isunique } +func UniqueAddress(networkName string, reverse bool) (net.IP, error) { + if servercfg.CacheEnabled() { + return UniqueAddressCache(networkName, reverse) + } + return UniqueAddressDB(networkName, reverse) +} -// UniqueAddress6 - see if ipv6 address is unique func UniqueAddress6(networkName string, reverse bool) (net.IP, error) { + if servercfg.CacheEnabled() { + return UniqueAddress6Cache(networkName, reverse) + } + return UniqueAddress6DB(networkName, reverse) +} + +// UniqueAddress6DB - see if ipv6 address is unique +func UniqueAddress6DB(networkName string, reverse bool) (net.IP, error) { + add := net.IP{} + var network models.Network + network, err := GetParentNetwork(networkName) + if err != nil { + fmt.Println("Network Not Found") + return add, err + } + if network.IsIPv6 == "no" { + return add, fmt.Errorf("IPv6 not active on network " + networkName) + } + + //ensure AddressRange is valid + if _, _, err := net.ParseCIDR(network.AddressRange6); err != nil { + return add, err + } + net6 := iplib.Net6FromStr(network.AddressRange6) + + newAddrs, err := net6.NextIP(net6.FirstAddress()) + if reverse { + newAddrs, err = net6.PreviousIP(net6.LastAddress()) + } + if err != nil { + return add, err + } + + for { + if IsIPUnique(networkName, newAddrs.String(), database.NODES_TABLE_NAME, true) && + IsIPUnique(networkName, newAddrs.String(), database.EXT_CLIENT_TABLE_NAME, true) { + return newAddrs, nil + } + if reverse { + newAddrs, err = net6.PreviousIP(newAddrs) + } else { + newAddrs, err = net6.NextIP(newAddrs) + } + if err != nil { + break + } + } + + return add, errors.New("ERROR: No unique IPv6 addresses available. Check network subnet") +} + +// UniqueAddress6Cache - see if ipv6 address is unique using cache +func UniqueAddress6Cache(networkName string, reverse bool) (net.IP, error) { add := net.IP{} var network models.Network network, err := GetParentNetwork(networkName) diff --git a/logic/nodes.go b/logic/nodes.go index 1146151e..c28da4dc 100644 --- a/logic/nodes.go +++ b/logic/nodes.go @@ -35,12 +35,20 @@ var ( func getNodeFromCache(nodeID string) (node models.Node, ok bool) { nodeCacheMutex.RLock() node, ok = nodesCacheMap[nodeID] + if node.Mutex == nil { + node.Mutex = &sync.Mutex{} + } nodeCacheMutex.RUnlock() return } func getNodesFromCache() (nodes []models.Node) { nodeCacheMutex.RLock() - nodes = slices.Collect(maps.Values(nodesCacheMap)) + for _, node := range nodesCacheMap { + if node.Mutex == nil { + node.Mutex = &sync.Mutex{} + } + nodes = append(nodes, node) + } nodeCacheMutex.RUnlock() return } @@ -356,12 +364,15 @@ func DeleteNodeByID(node *models.Node) error { logger.Log(1, "unable to remove metrics from DB for node", node.ID.String(), err.Error()) } //recycle ip address - if node.Address.IP != nil { - RemoveIpFromAllocatedIpMap(node.Network, node.Address.IP.String()) - } - if node.Address6.IP != nil { - RemoveIpFromAllocatedIpMap(node.Network, node.Address6.IP.String()) + if servercfg.CacheEnabled() { + if node.Address.IP != nil { + RemoveIpFromAllocatedIpMap(node.Network, node.Address.IP.String()) + } + if node.Address6.IP != nil { + RemoveIpFromAllocatedIpMap(node.Network, node.Address6.IP.String()) + } } + return nil } @@ -423,6 +434,9 @@ func GetAllNodes() ([]models.Node, error) { } // add node to our array nodes = append(nodes, node) + if node.Mutex == nil { + node.Mutex = &sync.Mutex{} + } nodesMap[node.ID.String()] = node } @@ -698,15 +712,16 @@ func createNode(node *models.Node) error { if servercfg.CacheEnabled() { storeNodeInCache(*node) storeNodeInNetworkCache(*node, node.Network) - } - if _, ok := allocatedIpMap[node.Network]; ok { - if node.Address.IP != nil { - AddIpToAllocatedIpMap(node.Network, node.Address.IP) - } - if node.Address6.IP != nil { - AddIpToAllocatedIpMap(node.Network, node.Address6.IP) + if _, ok := allocatedIpMap[node.Network]; ok { + if node.Address.IP != nil { + AddIpToAllocatedIpMap(node.Network, node.Address.IP) + } + if node.Address6.IP != nil { + AddIpToAllocatedIpMap(node.Network, node.Address6.IP) + } } } + _, err = nodeacls.CreateNodeACL(nodeacls.NetworkID(node.Network), nodeacls.NodeID(node.ID.String()), defaultACLVal) if err != nil { logger.Log(1, "failed to create node ACL for node,", node.ID.String(), "err:", err.Error()) @@ -752,16 +767,14 @@ func ValidateParams(nodeid, netid string) (models.Node, error) { func ValidateNodeIp(currentNode *models.Node, newNode *models.ApiNode) error { if currentNode.Address.IP != nil && currentNode.Address.String() != newNode.Address { - newIp, _, _ := net.ParseCIDR(newNode.Address) - ipAllocated := allocatedIpMap[currentNode.Network] - if _, ok := ipAllocated[newIp.String()]; ok { + if !IsIPUnique(newNode.Network, newNode.Address, database.NODES_TABLE_NAME, false) || + !IsIPUnique(newNode.Network, newNode.Address, database.EXT_CLIENT_TABLE_NAME, false) { return errors.New("ip specified is already allocated: " + newNode.Address) } } if currentNode.Address6.IP != nil && currentNode.Address6.String() != newNode.Address6 { - newIp, _, _ := net.ParseCIDR(newNode.Address6) - ipAllocated := allocatedIpMap[currentNode.Network] - if _, ok := ipAllocated[newIp.String()]; ok { + if !IsIPUnique(newNode.Network, newNode.Address6, database.NODES_TABLE_NAME, false) || + !IsIPUnique(newNode.Network, newNode.Address6, database.EXT_CLIENT_TABLE_NAME, false) { return errors.New("ip specified is already allocated: " + newNode.Address6) } } @@ -824,9 +837,16 @@ func GetTagMapWithNodes() (tagNodesMap map[models.TagID][]models.Node) { if nodeI.Tags == nil { continue } + if nodeI.Mutex != nil { + nodeI.Mutex.Lock() + } for nodeTagID := range nodeI.Tags { tagNodesMap[nodeTagID] = append(tagNodesMap[nodeTagID], nodeI) } + if nodeI.Mutex != nil { + nodeI.Mutex.Unlock() + } + } return } @@ -838,9 +858,15 @@ func GetTagMapWithNodesByNetwork(netID models.NetworkID, withStaticNodes bool) ( if nodeI.Tags == nil { continue } + if nodeI.Mutex != nil { + nodeI.Mutex.Lock() + } for nodeTagID := range nodeI.Tags { tagNodesMap[nodeTagID] = append(tagNodesMap[nodeTagID], nodeI) } + if nodeI.Mutex != nil { + nodeI.Mutex.Unlock() + } } tagNodesMap["*"] = nodes if !withStaticNodes { @@ -859,17 +885,16 @@ func AddTagMapWithStaticNodes(netID models.NetworkID, if extclient.Tags == nil || extclient.RemoteAccessClientID != "" { continue } - for tagID := range extclient.Tags { - tagNodesMap[tagID] = append(tagNodesMap[tagID], models.Node{ - IsStatic: true, - StaticNode: extclient, - }) - tagNodesMap["*"] = append(tagNodesMap["*"], models.Node{ - IsStatic: true, - StaticNode: extclient, - }) + if extclient.Mutex != nil { + extclient.Mutex.Lock() + } + for tagID := range extclient.Tags { + tagNodesMap[tagID] = append(tagNodesMap[tagID], extclient.ConvertToStaticNode()) + tagNodesMap["*"] = append(tagNodesMap["*"], extclient.ConvertToStaticNode()) + } + if extclient.Mutex != nil { + extclient.Mutex.Unlock() } - } return tagNodesMap } @@ -884,11 +909,14 @@ func AddTagMapWithStaticNodesWithUsers(netID models.NetworkID, if extclient.Tags == nil { continue } + if extclient.Mutex != nil { + extclient.Mutex.Lock() + } for tagID := range extclient.Tags { - tagNodesMap[tagID] = append(tagNodesMap[tagID], models.Node{ - IsStatic: true, - StaticNode: extclient, - }) + tagNodesMap[tagID] = append(tagNodesMap[tagID], extclient.ConvertToStaticNode()) + } + if extclient.Mutex != nil { + extclient.Mutex.Unlock() } } @@ -906,9 +934,15 @@ func GetNodesWithTag(tagID models.TagID) map[string]models.Node { if nodeI.Tags == nil { continue } + if nodeI.Mutex != nil { + nodeI.Mutex.Lock() + } if _, ok := nodeI.Tags[tagID]; ok { nMap[nodeI.ID.String()] = nodeI } + if nodeI.Mutex != nil { + nodeI.Mutex.Unlock() + } } return AddStaticNodesWithTag(tag, nMap) } @@ -922,13 +956,15 @@ func AddStaticNodesWithTag(tag models.Tag, nMap map[string]models.Node) map[stri if extclient.RemoteAccessClientID != "" { continue } - if _, ok := extclient.Tags[tag.ID]; ok { - nMap[extclient.ClientID] = models.Node{ - IsStatic: true, - StaticNode: extclient, - } + if extclient.Mutex != nil { + extclient.Mutex.Lock() + } + if _, ok := extclient.Tags[tag.ID]; ok { + nMap[extclient.ClientID] = extclient.ConvertToStaticNode() + } + if extclient.Mutex != nil { + extclient.Mutex.Unlock() } - } return nMap } @@ -944,10 +980,7 @@ func GetStaticNodeWithTag(tagID models.TagID) map[string]models.Node { return nMap } for _, extclient := range extclients { - nMap[extclient.ClientID] = models.Node{ - IsStatic: true, - StaticNode: extclient, - } + nMap[extclient.ClientID] = extclient.ConvertToStaticNode() } return nMap } diff --git a/models/extclient.go b/models/extclient.go index e9d3708b..0641cccf 100644 --- a/models/extclient.go +++ b/models/extclient.go @@ -1,5 +1,7 @@ package models +import "sync" + // ExtClient - struct for external clients type ExtClient struct { ClientID string `json:"clientid" bson:"clientid"` @@ -25,6 +27,7 @@ type ExtClient struct { DeviceName string `json:"device_name"` PublicEndpoint string `json:"public_endpoint"` Country string `json:"country"` + Mutex *sync.Mutex `json:"-"` } // CustomExtClient - struct for CustomExtClient params @@ -55,5 +58,6 @@ func (ext *ExtClient) ConvertToStaticNode() Node { Tags: ext.Tags, IsStatic: true, StaticNode: *ext, + Mutex: ext.Mutex, } } diff --git a/models/node.go b/models/node.go index 48243540..72dc9ea5 100644 --- a/models/node.go +++ b/models/node.go @@ -5,6 +5,7 @@ import ( "math/rand" "net" "strings" + "sync" "time" "github.com/google/uuid" @@ -119,6 +120,7 @@ type Node struct { IsUserNode bool `json:"is_user_node"` StaticNode ExtClient `json:"static_node"` Status NodeStatus `json:"node_status"` + Mutex *sync.Mutex `json:"-"` } // LegacyNode - legacy struct for node model diff --git a/pro/auth/auth.go b/pro/auth/auth.go index 49a34e73..162a6ce2 100644 --- a/pro/auth/auth.go +++ b/pro/auth/auth.go @@ -245,6 +245,12 @@ func getUserEmailFromClaims(token string) string { return "" } claims, _ := accessToken.Claims.(jwt.MapClaims) + if claims == nil { + return "" + } + if claims["email"] == nil { + return "" + } return claims["email"].(string) } diff --git a/pro/auth/azure-ad.go b/pro/auth/azure-ad.go index 7aa34953..8d059b46 100644 --- a/pro/auth/azure-ad.go +++ b/pro/auth/azure-ad.go @@ -199,6 +199,10 @@ func getAzureUserInfo(state string, code string) (*OAuthUser, error) { if userInfo.Email == "" { userInfo.Email = getUserEmailFromClaims(token.AccessToken) } + if userInfo.Email == "" && userInfo.UserPrincipalName != "" { + userInfo.Email = userInfo.UserPrincipalName + + } if userInfo.Email == "" { err = errors.New("failed to fetch user email from SSO state") return userInfo, err diff --git a/pro/controllers/users.go b/pro/controllers/users.go index 5cdc767c..f87fe020 100644 --- a/pro/controllers/users.go +++ b/pro/controllers/users.go @@ -1108,6 +1108,9 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) { } gws := userGws[node.Network] + if extClient.DNS == "" { + extClient.DNS = node.IngressDNS + } extClient.AllowedIPs = logic.GetExtclientAllowedIPs(extClient) gws = append(gws, models.UserRemoteGws{ GwID: node.ID.String(), diff --git a/pro/email/email.go b/pro/email/email.go index 7411ae3e..cde69826 100644 --- a/pro/email/email.go +++ b/pro/email/email.go @@ -55,6 +55,6 @@ func GetClient() (e EmailSender) { } func IsValid(email string) bool { - emailRegex := regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`) + emailRegex := regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}$`) return emailRegex.MatchString(email) }