package ncutils import ( "bytes" crand "crypto/rand" "crypto/tls" "encoding/gob" "errors" "fmt" "io" "log" "math/rand" "net" "net/http" "os" "os/exec" "path/filepath" "regexp" "runtime" "strconv" "strings" "time" "github.com/gravitl/netmaker/models" "golang.org/x/crypto/nacl/box" "golang.zx2c4.com/wireguard/wgctrl" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "google.golang.org/grpc" "google.golang.org/grpc/credentials" ) // Version - version of the netclient var Version = "dev" // src - for random strings var src = rand.NewSource(time.Now().UnixNano()) // MAX_NAME_LENGTH - maximum node name length const MAX_NAME_LENGTH = 62 // NO_DB_RECORD - error message result const NO_DB_RECORD = "no result found" // NO_DB_RECORDS - error record result const NO_DB_RECORDS = "could not find any records" // LINUX_APP_DATA_PATH - linux path const LINUX_APP_DATA_PATH = "/etc/netclient" // WINDOWS_APP_DATA_PATH - windows path const WINDOWS_APP_DATA_PATH = "C:\\ProgramData\\Netclient" // WINDOWS_APP_DATA_PATH - windows path const WINDOWS_WG_DPAPI_PATH = "C:\\Program Files\\WireGuard\\Data\\Configurations" // WINDOWS_SVC_NAME - service name const WINDOWS_SVC_NAME = "netclient" // NETCLIENT_DEFAULT_PORT - default port const NETCLIENT_DEFAULT_PORT = 51821 // DEFAULT_GC_PERCENT - garbage collection percent const DEFAULT_GC_PERCENT = 10 // KEY_SIZE = ideal length for keys const KEY_SIZE = 2048 // constants for random strings const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" const ( letterIdxBits = 6 // 6 bits to represent a letter index letterIdxMask = 1<> 2) } } return data, err } // GetNetclientPathSpecific - gets specific netclient config path func GetNetclientPathSpecific() string { if IsWindows() { return WINDOWS_APP_DATA_PATH + "\\" } else if IsMac() { return "/etc/netclient/config/" } else { return LINUX_APP_DATA_PATH + "/config/" } } // GetNewIface - Gets the name of the real interface created on Mac func GetNewIface(dir string) (string, error) { files, _ := os.ReadDir(dir) var newestFile string var newestTime int64 = 0 var err error for _, f := range files { fi, err := os.Stat(dir + f.Name()) if err != nil { return "", err } currTime := fi.ModTime().Unix() if currTime > newestTime && strings.Contains(f.Name(), ".sock") { newestTime = currTime newestFile = f.Name() } } resultArr := strings.Split(newestFile, ".") if resultArr[0] == "" { err = errors.New("sock file does not exist") } return resultArr[0], err } // GetFileAsString - returns the string contents of a given file func GetFileAsString(path string) (string, error) { content, err := os.ReadFile(path) if err != nil { return "", err } return string(content), err } // GetNetclientPathSpecific - gets specific netclient config path func GetWGPathSpecific() string { if IsWindows() { return WINDOWS_APP_DATA_PATH + "\\" } else { return "/etc/wireguard/" } } // GRPCRequestOpts - gets grps request opts func GRPCRequestOpts(isSecure string) grpc.DialOption { var requestOpts grpc.DialOption requestOpts = grpc.WithInsecure() if isSecure == "on" { h2creds := credentials.NewTLS(&tls.Config{NextProtos: []string{"h2"}}) requestOpts = grpc.WithTransportCredentials(h2creds) } return requestOpts } // Copy - copies a src file to dest func Copy(src, dst string) error { sourceFileStat, err := os.Stat(src) if err != nil { return err } if !sourceFileStat.Mode().IsRegular() { return errors.New(src + " is not a regular file") } source, err := os.Open(src) if err != nil { return err } defer source.Close() destination, err := os.Create(dst) if err != nil { return err } defer destination.Close() _, err = io.Copy(destination, source) if err != nil { return err } err = os.Chmod(dst, 0755) return err } // RunsCmds - runs cmds func RunCmds(commands []string, printerr bool) error { var err error for _, command := range commands { args := strings.Fields(command) out, err := exec.Command(args[0], args[1:]...).CombinedOutput() if err != nil && printerr { log.Println("error running command:", command) log.Println(strings.TrimSuffix(string(out), "\n")) } } return err } // FileExists - checks if file exists locally func FileExists(f string) bool { info, err := os.Stat(f) if os.IsNotExist(err) { return false } if err != nil && strings.Contains(err.Error(), "not a directory") { return false } if err != nil { Log("error reading file: " + f + ", " + err.Error()) } return !info.IsDir() } // PrintLog - prints log func PrintLog(message string, loglevel int) { log.SetFlags(log.Flags() &^ (log.Llongfile | log.Lshortfile)) if loglevel < 2 { log.Println("[netclient]", message) } } // GetSystemNetworks - get networks locally func GetSystemNetworks() ([]string, error) { var networks []string files, err := filepath.Glob(GetNetclientPathSpecific() + "netconfig-*") if err != nil { return nil, err } for _, file := range files { //don't want files such as *.bak, *.swp if filepath.Ext(file) != "" { continue } file := filepath.Base(file) temp := strings.Split(file, "-") networks = append(networks, strings.Join(temp[1:], "-")) } return networks, nil } func stringAfter(original string, substring string) string { position := strings.LastIndex(original, substring) if position == -1 { return "" } adjustedPosition := position + len(substring) if adjustedPosition >= len(original) { return "" } return original[adjustedPosition:] } // ShortenString - Brings string down to specified length. Stops names from being too long func ShortenString(input string, length int) string { output := input if len(input) > length { output = input[0:length] } return output } // DNSFormatString - Formats a string with correct usage for DNS func DNSFormatString(input string) string { reg, err := regexp.Compile("[^a-zA-Z0-9-]+") if err != nil { Log("error with regex: " + err.Error()) return "" } return reg.ReplaceAllString(input, "") } // GetHostname - Gets hostname of machine func GetHostname() string { hostname, err := os.Hostname() if err != nil { return "" } if len(hostname) > MAX_NAME_LENGTH { hostname = hostname[0:MAX_NAME_LENGTH] } return hostname } // CheckUID - Checks to make sure user has root privileges func CheckUID() { // start our application out, err := RunCmd("id -u", true) if err != nil { log.Fatal(out, err) } id, err := strconv.Atoi(string(out[:len(out)-1])) if err != nil { log.Fatal(err) } if id != 0 { log.Fatal("This program must be run with elevated privileges (sudo). This program installs a SystemD service and configures WireGuard and networking rules. Please re-run with sudo/root.") } } // CheckWG - Checks if WireGuard is installed. If not, exit func CheckWG() { var _, err = exec.LookPath("wg") uspace := GetWireGuard() if err != nil { if uspace == "wg" { PrintLog(err.Error(), 0) log.Fatal("WireGuard not installed. Please install WireGuard (wireguard-tools) and try again.") } PrintLog("running with userspace wireguard: "+uspace, 0) } else if uspace != "wg" { PrintLog("running userspace WireGuard with "+uspace, 0) } } // ConvertKeyToBytes - util to convert a key to bytes to use elsewhere func ConvertKeyToBytes(key *[32]byte) ([]byte, error) { var buffer bytes.Buffer var enc = gob.NewEncoder(&buffer) if err := enc.Encode(key); err != nil { return nil, err } return buffer.Bytes(), nil } // ConvertBytesToKey - util to convert bytes to a key to use elsewhere func ConvertBytesToKey(data []byte) (*[32]byte, error) { var buffer = bytes.NewBuffer(data) var dec = gob.NewDecoder(buffer) var result = new([32]byte) var err = dec.Decode(result) if err != nil { return nil, err } return result, err } // ServerAddrSliceContains - sees if a string slice contains a string element func ServerAddrSliceContains(slice []models.ServerAddr, item models.ServerAddr) bool { for _, s := range slice { if s.Address == item.Address && s.IsLeader == item.IsLeader { return true } } return false } // BoxEncrypt - encrypts traffic box func BoxEncrypt(message []byte, recipientPubKey *[32]byte, senderPrivateKey *[32]byte) ([]byte, error) { var nonce [24]byte // 192 bits of randomization if _, err := io.ReadFull(crand.Reader, nonce[:]); err != nil { return nil, err } encrypted := box.Seal(nonce[:], message, &nonce, recipientPubKey, senderPrivateKey) return encrypted, nil } // BoxDecrypt - decrypts traffic box func BoxDecrypt(encrypted []byte, senderPublicKey *[32]byte, recipientPrivateKey *[32]byte) ([]byte, error) { var decryptNonce [24]byte copy(decryptNonce[:], encrypted[:24]) decrypted, ok := box.Open(nil, encrypted[24:], &decryptNonce, senderPublicKey, recipientPrivateKey) if !ok { return nil, fmt.Errorf("could not decrypt message") } return decrypted, nil } // MakeRandomString - generates a random string of len n func MakeRandomString(n int) string { sb := strings.Builder{} sb.Grow(n) // A src.Int63() generates 63 random bits, enough for letterIdxMax characters! for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; { if remain == 0 { cache, remain = src.Int63(), letterIdxMax } if idx := int(cache & letterIdxMask); idx < len(letterBytes) { sb.WriteByte(letterBytes[idx]) i-- } cache >>= letterIdxBits remain-- } return sb.String() }