package main import ( "bufio" "bytes" "context" "crypto/x509" "encoding/json" "encoding/pem" "errors" "fmt" "github.com/google/uuid" "io/ioutil" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "net" "net/http" "os" "regexp" "strconv" "strings" "sync" "text/template" "time" "unicode/utf8" "github.com/gobuffalo/packr/v2" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" log "github.com/sirupsen/logrus" "gopkg.in/alecthomas/kingpin.v2" ) const ( usernameRegexp = `^([a-zA-Z0-9_.\-@])+$` passwordMinLength = 6 certsArchiveFileName = "certs.tar.gz" ccdArchiveFileName = "ccd.tar.gz" indexTxtDateLayout = "060102150405Z" stringDateFormat = "2006-01-02 15:04:05" downloadCertsApiUrl = "api/data/certs/download" downloadCcdApiUrl = "api/data/ccd/download" kubeNamespaceFilePath = "/var/run/secrets/kubernetes.io/serviceaccount/namespace" ) var ( listenHost = kingpin.Flag("listen.host", "host for ovpn-admin").Default("0.0.0.0").Envar("OVPN_LISTEN_HOST").String() listenPort = kingpin.Flag("listen.port", "port for ovpn-admin").Default("8080").Envar("OVPN_LISTEN_PORT").String() listenBaseUrl = kingpin.Flag("listen.base-url", "base url for ovpn-admin").Default("/").Envar("OVPN_LISTEN_BASE_URL").String() serverRole = kingpin.Flag("role", "server role, master or slave").Default("master").Envar("OVPN_ROLE").HintOptions("master", "slave").String() masterHost = kingpin.Flag("master.host", "URL for the master server").Default("http://127.0.0.1").Envar("OVPN_MASTER_HOST").String() masterBasicAuthUser = kingpin.Flag("master.basic-auth.user", "user for master server's Basic Auth").Default("").Envar("OVPN_MASTER_USER").String() masterBasicAuthPassword = kingpin.Flag("master.basic-auth.password", "password for master server's Basic Auth").Default("").Envar("OVPN_MASTER_PASSWORD").String() masterSyncFrequency = kingpin.Flag("master.sync-frequency", "master host data sync frequency in seconds").Default("600").Envar("OVPN_MASTER_SYNC_FREQUENCY").Int() masterSyncToken = kingpin.Flag("master.sync-token", "master host data sync security token").Default("VerySecureToken").Envar("OVPN_MASTER_TOKEN").PlaceHolder("TOKEN").String() openvpnNetwork = kingpin.Flag("ovpn.network", "NETWORK/MASK_PREFIX for OpenVPN server").Default("172.16.100.0/24").Envar("OVPN_NETWORK").String() openvpnServer = kingpin.Flag("ovpn.server", "HOST:PORT:PROTOCOL for OpenVPN server; can have multiple values").Default("127.0.0.1:7777:tcp").Envar("OVPN_SERVER").PlaceHolder("HOST:PORT:PROTOCOL").Strings() openvpnServerBehindLB = kingpin.Flag("ovpn.server.behindLB", "enable if your OpenVPN server is behind Kubernetes Service having the LoadBalancer type").Default("false").Envar("OVPN_LB").Bool() openvpnServiceName = kingpin.Flag("ovpn.service", "the name of Kubernetes Service having the LoadBalancer type if your OpenVPN server is behind it").Default("openvpn-external").Envar("OVPN_LB_SERVICE").Strings() mgmtAddress = kingpin.Flag("mgmt", "ALIAS=HOST:PORT for OpenVPN server mgmt interface; can have multiple values").Default("main=127.0.0.1:8989").Envar("OVPN_MGMT").Strings() metricsPath = kingpin.Flag("metrics.path", "URL path for exposing collected metrics").Default("/metrics").Envar("OVPN_METRICS_PATH").String() easyrsaDirPath = kingpin.Flag("easyrsa.path", "path to easyrsa dir").Default("./easyrsa").Envar("EASYRSA_PATH").String() indexTxtPath = kingpin.Flag("easyrsa.index-path", "path to easyrsa index file").Default("").Envar("OVPN_INDEX_PATH").String() easyrsaBinPath = kingpin.Flag("easyrsa.bin-path", "path to easyrsa script").Default("easyrsa").Envar("EASYRSA_BIN_PATH").String() ccdEnabled = kingpin.Flag("ccd", "enable client-config-dir").Default("false").Envar("OVPN_CCD").Bool() ccdDir = kingpin.Flag("ccd.path", "path to client-config-dir").Default("./ccd").Envar("OVPN_CCD_PATH").String() clientConfigTemplatePath = kingpin.Flag("templates.clientconfig-path", "path to custom client.conf.tpl").Default("").Envar("OVPN_TEMPLATES_CC_PATH").String() ccdTemplatePath = kingpin.Flag("templates.ccd-path", "path to custom ccd.tpl").Default("").Envar("OVPN_TEMPLATES_CCD_PATH").String() authByPassword = kingpin.Flag("auth.password", "enable additional password authentication").Default("false").Envar("OVPN_AUTH").Bool() authDatabase = kingpin.Flag("auth.db", "database path for password authentication").Default("./easyrsa/pki/users.db").Envar("OVPN_AUTH_DB_PATH").String() logLevel = kingpin.Flag("log.level", "set log level: trace, debug, info, warn, error (default info)").Default("info").Envar("LOG_LEVEL").String() logFormat = kingpin.Flag("log.format", "set log format: text, json (default text)").Default("text").Envar("LOG_FORMAT").String() storageBackend = kingpin.Flag("storage.backend", "storage backend: filesystem, kubernetes.secrets (default filesystem)").Default("filesystem").Envar("STORAGE_BACKEND").String() certsArchivePath = "/tmp/" + certsArchiveFileName ccdArchivePath = "/tmp/" + ccdArchiveFileName version = "2.0.0" ) var logLevels = map[string]log.Level{ "trace": log.TraceLevel, "debug": log.DebugLevel, "info": log.InfoLevel, "warn": log.WarnLevel, "error": log.ErrorLevel, } var logFormats = map[string]log.Formatter{ "text": &log.TextFormatter{}, "json": &log.JSONFormatter{}, } var ( ovpnServerCertExpire = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "ovpn_server_cert_expire", Help: "openvpn server certificate expire time in days", }, ) ovpnServerCaCertExpire = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "ovpn_server_ca_cert_expire", Help: "openvpn server CA certificate expire time in days", }, ) ovpnClientsTotal = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "ovpn_clients_total", Help: "total openvpn users", }, ) ovpnClientsRevoked = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "ovpn_clients_revoked", Help: "revoked openvpn users", }, ) ovpnClientsExpired = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "ovpn_clients_expired", Help: "expired openvpn users", }, ) ovpnClientsConnected = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "ovpn_clients_connected", Help: "total connected openvpn clients", }, ) ovpnUniqClientsConnected = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "ovpn_uniq_clients_connected", Help: "uniq connected openvpn clients", }, ) ovpnClientCertificateExpire = prometheus.NewGaugeVec(prometheus.GaugeOpts{ Name: "ovpn_client_cert_expire", Help: "openvpn user certificate expire time in days", }, []string{"client"}, ) ovpnClientConnectionInfo = prometheus.NewGaugeVec(prometheus.GaugeOpts{ Name: "ovpn_client_connection_info", Help: "openvpn user connection info. ip - assigned address from ovpn network. value - last time when connection was refreshed in unix format", }, []string{"client", "ip"}, ) ovpnClientConnectionFrom = prometheus.NewGaugeVec(prometheus.GaugeOpts{ Name: "ovpn_client_connection_from", Help: "openvpn user connection info. ip - from which address connection was initialized. value - time when connection was initialized in unix format", }, []string{"client", "ip"}, ) ovpnClientBytesReceived = prometheus.NewGaugeVec(prometheus.GaugeOpts{ Name: "ovpn_client_bytes_received", Help: "openvpn user bytes received", }, []string{"client"}, ) ovpnClientBytesSent = prometheus.NewGaugeVec(prometheus.GaugeOpts{ Name: "ovpn_client_bytes_sent", Help: "openvpn user bytes sent", }, []string{"client"}, ) ) type OvpnAdmin struct { role string lastSyncTime string lastSuccessfulSyncTime string masterHostBasicAuth bool masterSyncToken string clients []OpenvpnClient activeClients []clientStatus promRegistry *prometheus.Registry mgmtInterfaces map[string]string templates *packr.Box modules []string mgmtStatusTimeFormat string createUserMutex *sync.Mutex } type OpenvpnServer struct { Host string Port string Protocol string } type openvpnClientConfig struct { Hosts []OpenvpnServer CA string Cert string Key string TLS string PasswdAuth bool } type OpenvpnClient struct { Identity string `json:"Identity"` AccountStatus string `json:"AccountStatus"` ExpirationDate string `json:"ExpirationDate"` RevocationDate string `json:"RevocationDate"` ConnectionStatus string `json:"ConnectionStatus"` Connections int `json:"Connections"` } type ccdRoute struct { Address string `json:"Address"` Mask string `json:"Mask"` Description string `json:"Description"` } type Ccd struct { User string `json:"User"` ClientAddress string `json:"ClientAddress"` CustomRoutes []ccdRoute `json:"CustomRoutes"` } type indexTxtLine struct { Flag string ExpirationDate string RevocationDate string SerialNumber string Filename string DistinguishedName string Identity string } type clientStatus struct { CommonName string RealAddress string BytesReceived string BytesSent string ConnectedSince string VirtualAddress string LastRef string ConnectedSinceFormatted string LastRefFormatted string ConnectedTo string } func (oAdmin *OvpnAdmin) userListHandler(w http.ResponseWriter, r *http.Request) { log.Info(r.RemoteAddr, " ", r.RequestURI) if *storageBackend == "kubernetes.secrets" { err := app.updateIndexTxtOnDisk() if err != nil { log.Errorln(err) } oAdmin.clients = oAdmin.usersList() } usersList, _ := json.Marshal(oAdmin.clients) fmt.Fprintf(w, "%s", usersList) } func (oAdmin *OvpnAdmin) userStatisticHandler(w http.ResponseWriter, r *http.Request) { log.Info(r.RemoteAddr, " ", r.RequestURI) _ = r.ParseForm() userStatistic, _ := json.Marshal(oAdmin.getUserStatistic(r.FormValue("username"))) fmt.Fprintf(w, "%s", userStatistic) } func (oAdmin *OvpnAdmin) userCreateHandler(w http.ResponseWriter, r *http.Request) { log.Info(r.RemoteAddr, " ", r.RequestURI) if oAdmin.role == "slave" { http.Error(w, `{"status":"error"}`, http.StatusLocked) return } _ = r.ParseForm() userCreated, userCreateStatus := oAdmin.userCreate(r.FormValue("username"), r.FormValue("password")) if userCreated { oAdmin.clients = oAdmin.usersList() w.WriteHeader(http.StatusOK) fmt.Fprintf(w, userCreateStatus) return } else { http.Error(w, userCreateStatus, http.StatusUnprocessableEntity) } } func (oAdmin *OvpnAdmin) userRotateHandler(w http.ResponseWriter, r *http.Request) { log.Info(r.RemoteAddr, " ", r.RequestURI) if oAdmin.role == "slave" { http.Error(w, `{"status":"error"}`, http.StatusLocked) return } _ = r.ParseForm() err, msg := oAdmin.userRotate(r.FormValue("username"), r.FormValue("password")) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) } else { w.WriteHeader(http.StatusOK) fmt.Fprintf(w, msg) } } func (oAdmin *OvpnAdmin) userDeleteHandler(w http.ResponseWriter, r *http.Request) { log.Info(r.RemoteAddr, " ", r.RequestURI) if oAdmin.role == "slave" { http.Error(w, `{"status":"error"}`, http.StatusLocked) return } _ = r.ParseForm() err, msg := oAdmin.userDelete(r.FormValue("username")) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) } else { w.WriteHeader(http.StatusOK) fmt.Fprintf(w, msg) } } func (oAdmin *OvpnAdmin) userRevokeHandler(w http.ResponseWriter, r *http.Request) { log.Info(r.RemoteAddr, " ", r.RequestURI) if oAdmin.role == "slave" { http.Error(w, `{"status":"error"}`, http.StatusLocked) return } _ = r.ParseForm() err, msg := oAdmin.userRevoke(r.FormValue("username")) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) } else { w.WriteHeader(http.StatusOK) fmt.Fprintf(w, msg) } } func (oAdmin *OvpnAdmin) userUnrevokeHandler(w http.ResponseWriter, r *http.Request) { log.Info(r.RemoteAddr, " ", r.RequestURI) if oAdmin.role == "slave" { http.Error(w, `{"status":"error"}`, http.StatusLocked) return } _ = r.ParseForm() err, msg := oAdmin.userUnrevoke(r.FormValue("username")) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) } else { w.WriteHeader(http.StatusOK) fmt.Fprintf(w, msg) } } func (oAdmin *OvpnAdmin) userChangePasswordHandler(w http.ResponseWriter, r *http.Request) { log.Info(r.RemoteAddr, " ", r.RequestURI) _ = r.ParseForm() if *authByPassword { err, msg := oAdmin.userChangePassword(r.FormValue("username"), r.FormValue("password")) if err != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, `{"status":"error", "message": "%s"}`, msg) } else { w.WriteHeader(http.StatusOK) fmt.Fprintf(w, `{"status":"ok", "message": "%s"}`, msg) } } else { http.Error(w, `{"status":"error"}`, http.StatusNotImplemented) } } func (oAdmin *OvpnAdmin) userShowConfigHandler(w http.ResponseWriter, r *http.Request) { log.Info(r.RemoteAddr, " ", r.RequestURI) _ = r.ParseForm() fmt.Fprintf(w, "%s", oAdmin.renderClientConfig(r.FormValue("username"))) } func (oAdmin *OvpnAdmin) userDisconnectHandler(w http.ResponseWriter, r *http.Request) { log.Info(r.RemoteAddr, " ", r.RequestURI) _ = r.ParseForm() // fmt.Fprintf(w, "%s", userDisconnect(r.FormValue("username"))) fmt.Fprintf(w, "%s", r.FormValue("username")) } func (oAdmin *OvpnAdmin) userShowCcdHandler(w http.ResponseWriter, r *http.Request) { log.Info(r.RemoteAddr, " ", r.RequestURI) _ = r.ParseForm() ccd, _ := json.Marshal(oAdmin.getCcd(r.FormValue("username"))) fmt.Fprintf(w, "%s", ccd) } func (oAdmin *OvpnAdmin) userApplyCcdHandler(w http.ResponseWriter, r *http.Request) { log.Info(r.RemoteAddr, " ", r.RequestURI) if oAdmin.role == "slave" { http.Error(w, `{"status":"error"}`, http.StatusLocked) return } var ccd Ccd if r.Body == nil { http.Error(w, "Please send a request body", http.StatusBadRequest) return } err := json.NewDecoder(r.Body).Decode(&ccd) if err != nil { log.Errorln(err) } ccdApplied, applyStatus := oAdmin.modifyCcd(ccd) if ccdApplied { w.WriteHeader(http.StatusOK) fmt.Fprintf(w, applyStatus) return } else { http.Error(w, applyStatus, http.StatusUnprocessableEntity) } } func (oAdmin *OvpnAdmin) serverSettingsHandler(w http.ResponseWriter, r *http.Request) { log.Info(r.RemoteAddr, " ", r.RequestURI) enabledModules, enabledModulesErr := json.Marshal(oAdmin.modules) if enabledModulesErr != nil { log.Errorln(enabledModulesErr) } fmt.Fprintf(w, `{"status":"ok", "serverRole": "%s", "modules": %s }`, oAdmin.role, string(enabledModules)) } func (oAdmin *OvpnAdmin) lastSyncTimeHandler(w http.ResponseWriter, r *http.Request) { log.Debug(r.RemoteAddr, " ", r.RequestURI) fmt.Fprint(w, oAdmin.lastSyncTime) } func (oAdmin *OvpnAdmin) lastSuccessfulSyncTimeHandler(w http.ResponseWriter, r *http.Request) { log.Debug(r.RemoteAddr, " ", r.RequestURI) fmt.Fprint(w, oAdmin.lastSuccessfulSyncTime) } func (oAdmin *OvpnAdmin) downloadCertsHandler(w http.ResponseWriter, r *http.Request) { log.Info(r.RemoteAddr, " ", r.RequestURI) if oAdmin.role == "slave" { http.Error(w, `{"status":"error"}`, http.StatusBadRequest) return } if *storageBackend == "kubernetes.secrets" { http.Error(w, `{"status":"error"}`, http.StatusBadRequest) return } _ = r.ParseForm() token := r.Form.Get("token") if token != oAdmin.masterSyncToken { http.Error(w, `{"status":"error"}`, http.StatusForbidden) return } archiveCerts() w.Header().Set("Content-Disposition", "attachment; filename="+certsArchiveFileName) http.ServeFile(w, r, certsArchivePath) } func (oAdmin *OvpnAdmin) downloadCcdHandler(w http.ResponseWriter, r *http.Request) { log.Info(r.RemoteAddr, " ", r.RequestURI) if oAdmin.role == "slave" { http.Error(w, `{"status":"error"}`, http.StatusBadRequest) return } if *storageBackend == "kubernetes.secrets" { http.Error(w, `{"status":"error"}`, http.StatusBadRequest) return } _ = r.ParseForm() token := r.Form.Get("token") if token != oAdmin.masterSyncToken { http.Error(w, `{"status":"error"}`, http.StatusForbidden) return } archiveCcd() w.Header().Set("Content-Disposition", "attachment; filename="+ccdArchiveFileName) http.ServeFile(w, r, ccdArchivePath) } var app OpenVPNPKI func main() { kingpin.Version(version) kingpin.Parse() log.SetLevel(logLevels[*logLevel]) log.SetFormatter(logFormats[*logFormat]) if *storageBackend == "kubernetes.secrets" { err := app.run() if err != nil { log.Error(err) } } if *indexTxtPath == "" { *indexTxtPath = *easyrsaDirPath + "/pki/index.txt" } ovpnAdmin := new(OvpnAdmin) ovpnAdmin.lastSyncTime = "unknown" ovpnAdmin.role = *serverRole ovpnAdmin.lastSuccessfulSyncTime = "unknown" ovpnAdmin.masterSyncToken = *masterSyncToken ovpnAdmin.promRegistry = prometheus.NewRegistry() ovpnAdmin.modules = []string{} ovpnAdmin.createUserMutex = &sync.Mutex{} ovpnAdmin.mgmtInterfaces = make(map[string]string) for _, mgmtInterface := range *mgmtAddress { parts := strings.SplitN(mgmtInterface, "=", 2) ovpnAdmin.mgmtInterfaces[parts[0]] = parts[len(parts)-1] } ovpnAdmin.mgmtSetTimeFormat() ovpnAdmin.registerMetrics() ovpnAdmin.setState() go ovpnAdmin.updateState() if *masterBasicAuthPassword != "" && *masterBasicAuthUser != "" { ovpnAdmin.masterHostBasicAuth = true } else { ovpnAdmin.masterHostBasicAuth = false } ovpnAdmin.modules = append(ovpnAdmin.modules, "core") if *authByPassword { if *storageBackend != "kubernetes.secrets" { ovpnAdmin.modules = append(ovpnAdmin.modules, "passwdAuth") } else { log.Fatal("Right now the keys `--storage.backend=kubernetes.secret` and `--auth.password` are not working together. Please use only one of them ") } } if *ccdEnabled { ovpnAdmin.modules = append(ovpnAdmin.modules, "ccd") } if ovpnAdmin.role == "slave" { ovpnAdmin.syncDataFromMaster() go ovpnAdmin.syncWithMaster() } ovpnAdmin.templates = packr.New("template", "./templates") staticBox := packr.New("static", "./frontend/static") static := CacheControlWrapper(http.FileServer(staticBox)) http.Handle(*listenBaseUrl, http.StripPrefix(strings.TrimRight(*listenBaseUrl, "/"), static)) http.HandleFunc(*listenBaseUrl + "api/server/settings", ovpnAdmin.serverSettingsHandler) http.HandleFunc(*listenBaseUrl + "api/users/list", ovpnAdmin.userListHandler) http.HandleFunc(*listenBaseUrl + "api/user/create", ovpnAdmin.userCreateHandler) http.HandleFunc(*listenBaseUrl + "api/user/change-password", ovpnAdmin.userChangePasswordHandler) http.HandleFunc(*listenBaseUrl + "api/user/rotate", ovpnAdmin.userRotateHandler) http.HandleFunc(*listenBaseUrl + "api/user/delete", ovpnAdmin.userDeleteHandler) http.HandleFunc(*listenBaseUrl + "api/user/revoke", ovpnAdmin.userRevokeHandler) http.HandleFunc(*listenBaseUrl + "api/user/unrevoke", ovpnAdmin.userUnrevokeHandler) http.HandleFunc(*listenBaseUrl + "api/user/config/show", ovpnAdmin.userShowConfigHandler) http.HandleFunc(*listenBaseUrl + "api/user/disconnect", ovpnAdmin.userDisconnectHandler) http.HandleFunc(*listenBaseUrl + "api/user/statistic", ovpnAdmin.userStatisticHandler) http.HandleFunc(*listenBaseUrl + "api/user/ccd", ovpnAdmin.userShowCcdHandler) http.HandleFunc(*listenBaseUrl + "api/user/ccd/apply", ovpnAdmin.userApplyCcdHandler) http.HandleFunc(*listenBaseUrl + "api/sync/last/try", ovpnAdmin.lastSyncTimeHandler) http.HandleFunc(*listenBaseUrl + "api/sync/last/successful", ovpnAdmin.lastSuccessfulSyncTimeHandler) http.HandleFunc(*listenBaseUrl + downloadCertsApiUrl, ovpnAdmin.downloadCertsHandler) http.HandleFunc(*listenBaseUrl + downloadCcdApiUrl, ovpnAdmin.downloadCcdHandler) http.Handle(*metricsPath, promhttp.HandlerFor(ovpnAdmin.promRegistry, promhttp.HandlerOpts{})) http.HandleFunc(*listenBaseUrl + "ping", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "pong") }) log.Printf("Bind: http://%s:%s%s", *listenHost, *listenPort, *listenBaseUrl) log.Fatal(http.ListenAndServe(*listenHost+":"+*listenPort, nil)) } func CacheControlWrapper(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "max-age=2592000") // 30 days h.ServeHTTP(w, r) }) } func (oAdmin *OvpnAdmin) registerMetrics() { oAdmin.promRegistry.MustRegister(ovpnServerCertExpire) oAdmin.promRegistry.MustRegister(ovpnServerCaCertExpire) oAdmin.promRegistry.MustRegister(ovpnClientsTotal) oAdmin.promRegistry.MustRegister(ovpnClientsRevoked) oAdmin.promRegistry.MustRegister(ovpnClientsConnected) oAdmin.promRegistry.MustRegister(ovpnUniqClientsConnected) oAdmin.promRegistry.MustRegister(ovpnClientsExpired) oAdmin.promRegistry.MustRegister(ovpnClientCertificateExpire) oAdmin.promRegistry.MustRegister(ovpnClientConnectionInfo) oAdmin.promRegistry.MustRegister(ovpnClientConnectionFrom) oAdmin.promRegistry.MustRegister(ovpnClientBytesReceived) oAdmin.promRegistry.MustRegister(ovpnClientBytesSent) } func (oAdmin *OvpnAdmin) setState() { oAdmin.activeClients = oAdmin.mgmtGetActiveClients() oAdmin.clients = oAdmin.usersList() ovpnServerCaCertExpire.Set(float64((getOvpnCaCertExpireDate().Unix() - time.Now().Unix()) / 3600 / 24)) } func (oAdmin *OvpnAdmin) updateState() { for { time.Sleep(time.Duration(28) * time.Second) ovpnClientBytesSent.Reset() ovpnClientBytesReceived.Reset() ovpnClientConnectionFrom.Reset() ovpnClientConnectionInfo.Reset() ovpnClientCertificateExpire.Reset() go oAdmin.setState() } } func indexTxtParser(txt string) []indexTxtLine { var indexTxt []indexTxtLine txtLinesArray := strings.Split(txt, "\n") for _, v := range txtLinesArray { str := strings.Fields(v) if len(str) > 0 { switch { // case strings.HasPrefix(str[0], "E"): case strings.HasPrefix(str[0], "V"): indexTxt = append(indexTxt, indexTxtLine{Flag: str[0], ExpirationDate: str[1], SerialNumber: str[2], Filename: str[3], DistinguishedName: str[4], Identity: str[4][strings.Index(str[4], "=")+1:]}) case strings.HasPrefix(str[0], "R"): indexTxt = append(indexTxt, indexTxtLine{Flag: str[0], ExpirationDate: str[1], RevocationDate: str[2], SerialNumber: str[3], Filename: str[4], DistinguishedName: str[5], Identity: str[5][strings.Index(str[5], "=")+1:]}) } } } return indexTxt } func renderIndexTxt(data []indexTxtLine) string { indexTxt := "" for _, line := range data { switch { case line.Flag == "V": indexTxt += fmt.Sprintf("%s\t%s\t\t%s\t%s\t%s\n", line.Flag, line.ExpirationDate, line.SerialNumber, line.Filename, line.DistinguishedName) case line.Flag == "R": indexTxt += fmt.Sprintf("%s\t%s\t%s\t%s\t%s\t%s\n", line.Flag, line.ExpirationDate, line.RevocationDate, line.SerialNumber, line.Filename, line.DistinguishedName) // case line.flag == "E": } } return indexTxt } func (oAdmin *OvpnAdmin) getClientConfigTemplate() *template.Template { if *clientConfigTemplatePath != "" { return template.Must(template.ParseFiles(*clientConfigTemplatePath)) } else { clientConfigTpl, clientConfigTplErr := oAdmin.templates.FindString("client.conf.tpl") if clientConfigTplErr != nil { log.Error("clientConfigTpl not found in templates box") } return template.Must(template.New("client-config").Parse(clientConfigTpl)) } } func (oAdmin *OvpnAdmin) renderClientConfig(username string) string { if checkUserExist(username) { var hosts []OpenvpnServer for _, server := range *openvpnServer { parts := strings.SplitN(server, ":", 3) hosts = append(hosts, OpenvpnServer{Host: parts[0], Port: parts[1], Protocol: parts[2]}) } if *openvpnServerBehindLB { var err error hosts, err = getOvpnServerHostsFromKubeApi() if err != nil { log.Error(err) } } log.Tracef("hosts for %s\n %v", username, hosts) conf := openvpnClientConfig{} conf.Hosts = hosts conf.CA = fRead(*easyrsaDirPath + "/pki/ca.crt") conf.TLS = fRead(*easyrsaDirPath + "/pki/ta.key") if *storageBackend == "kubernetes.secrets" { conf.Cert, conf.Key = app.easyrsaGetClientCert(username) } else { conf.Cert = fRead(*easyrsaDirPath + "/pki/issued/" + username + ".crt") conf.Key = fRead(*easyrsaDirPath + "/pki/private/" + username + ".key") } conf.PasswdAuth = *authByPassword t := oAdmin.getClientConfigTemplate() var tmp bytes.Buffer err := t.Execute(&tmp, conf) if err != nil { log.Errorf("something goes wrong during rendering config for %s", username) log.Debugf("rendering config for %s failed with error %v", username, err) } hosts = nil log.Tracef("Rendered config for user %s: %+v", username, tmp.String()) return fmt.Sprintf("%+v", tmp.String()) } log.Warnf("user \"%s\" not found", username) return fmt.Sprintf("user \"%s\" not found", username) } func (oAdmin *OvpnAdmin) getCcdTemplate() *template.Template { if *ccdTemplatePath != "" { return template.Must(template.ParseFiles(*ccdTemplatePath)) } else { ccdTpl, ccdTplErr := oAdmin.templates.FindString("ccd.tpl") if ccdTplErr != nil { log.Errorf("ccdTpl not found in templates box") } return template.Must(template.New("ccd").Parse(ccdTpl)) } } func (oAdmin *OvpnAdmin) parseCcd(username string) Ccd { ccd := Ccd{} ccd.User = username ccd.ClientAddress = "dynamic" ccd.CustomRoutes = []ccdRoute{} var txtLinesArray []string if *storageBackend == "kubernetes.secrets" { txtLinesArray = strings.Split(app.secretGetCcd(ccd.User), "\n") } else { if fExist(*ccdDir + "/" + username) { txtLinesArray = strings.Split(fRead(*ccdDir+"/"+username), "\n") } } for _, v := range txtLinesArray { str := strings.Fields(v) if len(str) > 0 { switch { case strings.HasPrefix(str[0], "ifconfig-push"): ccd.ClientAddress = str[1] case strings.HasPrefix(str[0], "push"): ccd.CustomRoutes = append(ccd.CustomRoutes, ccdRoute{Address: strings.Trim(str[2], "\""), Mask: strings.Trim(str[3], "\""), Description: strings.Trim(strings.Join(str[4:], ""), "#")}) } } } return ccd } func (oAdmin *OvpnAdmin) modifyCcd(ccd Ccd) (bool, string) { ccdValid, err := validateCcd(ccd) if err != "" { return false, err } if ccdValid { t := oAdmin.getCcdTemplate() var tmp bytes.Buffer err := t.Execute(&tmp, ccd) if err != nil { log.Error(err) } if *storageBackend == "kubernetes.secrets" { app.secretUpdateCcd(ccd.User, tmp.Bytes()) } else { err = fWrite(*ccdDir+"/"+ccd.User, tmp.String()) if err != nil { log.Errorf("modifyCcd: fWrite(): %v", err) } } return true, "ccd updated successfully" } return false, "something goes wrong" } func validateCcd(ccd Ccd) (bool, string) { ccdErr := "" if ccd.ClientAddress != "dynamic" { _, ovpnNet, err := net.ParseCIDR(*openvpnNetwork) if err != nil { log.Error(err) } if !checkStaticAddressIsFree(ccd.ClientAddress, ccd.User) { ccdErr = fmt.Sprintf("ClientAddress \"%s\" already assigned to another user", ccd.ClientAddress) log.Debugf("modify ccd for user %s: %s", ccd.User, ccdErr) return false, ccdErr } if net.ParseIP(ccd.ClientAddress) == nil { ccdErr = fmt.Sprintf("ClientAddress \"%s\" not a valid IP address", ccd.ClientAddress) log.Debugf("modify ccd for user %s: %s", ccd.User, ccdErr) return false, ccdErr } if !ovpnNet.Contains(net.ParseIP(ccd.ClientAddress)) { ccdErr = fmt.Sprintf("ClientAddress \"%s\" not belongs to openvpn server network", ccd.ClientAddress) log.Debugf("modify ccd for user %s: %s", ccd.User, ccdErr) return false, ccdErr } } for _, route := range ccd.CustomRoutes { if net.ParseIP(route.Address) == nil { ccdErr = fmt.Sprintf("CustomRoute.Address \"%s\" must be a valid IP address", route.Address) log.Debugf("modify ccd for user %s: %s", ccd.User, ccdErr) return false, ccdErr } if net.ParseIP(route.Mask) == nil { ccdErr = fmt.Sprintf("CustomRoute.Mask \"%s\" must be a valid IP address", route.Mask) log.Debugf("modify ccd for user %s: %s", ccd.User, ccdErr) return false, ccdErr } } return true, ccdErr } func (oAdmin *OvpnAdmin) getCcd(username string) Ccd { ccd := Ccd{} ccd.User = username ccd.ClientAddress = "dynamic" ccd.CustomRoutes = []ccdRoute{} ccd = oAdmin.parseCcd(username) return ccd } func checkStaticAddressIsFree(staticAddress string, username string) bool { o := runBash(fmt.Sprintf("grep -rl ' %[1]s ' %[2]s | grep -vx %[2]s/%[3]s | wc -l", staticAddress, *ccdDir, username)) if strings.TrimSpace(o) == "0" { return true } return false } func validateUsername(username string) error { var validUsername = regexp.MustCompile(usernameRegexp) if validUsername.MatchString(username) { return nil } else { return errors.New(fmt.Sprintf("Username can only contains %s", usernameRegexp)) } } func validatePassword(password string) error { if utf8.RuneCountInString(password) < passwordMinLength { return errors.New(fmt.Sprintf("Password too short, password length must be greater or equal %d", passwordMinLength)) } else { return nil } } func checkUserExist(username string) bool { for _, u := range indexTxtParser(fRead(*indexTxtPath)) { if u.DistinguishedName == ("/CN=" + username) { return true } } return false } func (oAdmin *OvpnAdmin) usersList() []OpenvpnClient { var users []OpenvpnClient totalCerts := 0 validCerts := 0 revokedCerts := 0 expiredCerts := 0 connectedUniqUsers := 0 totalActiveConnections := 0 apochNow := time.Now().Unix() for _, line := range indexTxtParser(fRead(*indexTxtPath)) { if line.Identity != "server" && !strings.Contains(line.Identity, "REVOKED") { totalCerts += 1 ovpnClient := OpenvpnClient{Identity: line.Identity, ExpirationDate: parseDateToString(indexTxtDateLayout, line.ExpirationDate, stringDateFormat)} switch { case line.Flag == "V": ovpnClient.AccountStatus = "Active" validCerts += 1 case line.Flag == "R": ovpnClient.AccountStatus = "Revoked" ovpnClient.RevocationDate = parseDateToString(indexTxtDateLayout, line.RevocationDate, stringDateFormat) revokedCerts += 1 case line.Flag == "E": ovpnClient.AccountStatus = "Expired" expiredCerts += 1 } ovpnClientCertificateExpire.WithLabelValues(line.Identity).Set(float64((parseDateToUnix(indexTxtDateLayout, line.ExpirationDate) - apochNow) / 3600 / 24)) if (parseDateToUnix(indexTxtDateLayout, line.ExpirationDate) - apochNow) < 0 { ovpnClient.AccountStatus = "Expired" } ovpnClient.Connections = 0 userConnected, userConnectedTo := isUserConnected(line.Identity, oAdmin.activeClients) if userConnected { ovpnClient.ConnectionStatus = "Connected" for range userConnectedTo { ovpnClient.Connections += 1 totalActiveConnections += 1 } connectedUniqUsers += 1 } users = append(users, ovpnClient) } else { ovpnServerCertExpire.Set(float64((parseDateToUnix(indexTxtDateLayout, line.ExpirationDate) - apochNow) / 3600 / 24)) } } otherCerts := totalCerts - validCerts - revokedCerts - expiredCerts if otherCerts != 0 { log.Warnf("there are %d otherCerts", otherCerts) } ovpnClientsTotal.Set(float64(totalCerts)) ovpnClientsRevoked.Set(float64(revokedCerts)) ovpnClientsExpired.Set(float64(expiredCerts)) ovpnClientsConnected.Set(float64(totalActiveConnections)) ovpnUniqClientsConnected.Set(float64(connectedUniqUsers)) return users } func (oAdmin *OvpnAdmin) userCreate(username, password string) (bool, string) { ucErr := fmt.Sprintf("User \"%s\" created", username) oAdmin.createUserMutex.Lock() defer oAdmin.createUserMutex.Unlock() if checkUserExist(username) { ucErr = fmt.Sprintf("User \"%s\" already exists\n", username) log.Debugf("userCreate: checkUserExist(): %s", ucErr) return false, ucErr } if err := validateUsername(username); err != nil { log.Debugf("userCreate: validateUsername(): %s", err.Error()) return false, err.Error() } if *authByPassword { if err := validatePassword(password); err != nil { log.Debugf("userCreate: authByPassword(): %s", err.Error()) return false, err.Error() } } if *storageBackend == "kubernetes.secrets" { err := app.easyrsaBuildClient(username) if err != nil { log.Error(err) } } else { o := runBash(fmt.Sprintf("cd %s && %s build-client-full %s nopass 1>/dev/null", *easyrsaDirPath, *easyrsaBinPath, username)) log.Debug(o) } if *authByPassword { o := runBash(fmt.Sprintf("openvpn-user create --db.path %s --user %s --password %s", *authDatabase, username, password)) log.Debug(o) } log.Infof("Certificate for user %s issued", username) //oAdmin.clients = oAdmin.usersList() return true, ucErr } func (oAdmin *OvpnAdmin) userChangePassword(username, password string) (error, string) { if checkUserExist(username) { o := runBash(fmt.Sprintf("openvpn-user check --db.path %[1]s --user %[2]s | grep %[2]s | wc -l", *authDatabase, username)) log.Debug(o) if err := validatePassword(password); err != nil { log.Warningf("userChangePassword: %s", err.Error()) return err, err.Error() } if strings.TrimSpace(o) == "0" { o = runBash(fmt.Sprintf("openvpn-user create --db.path %s --user %s --password %s", *authDatabase, username, password)) log.Debug(o) } o = runBash(fmt.Sprintf("openvpn-user change-password --db.path %s --user %s --password %s", *authDatabase, username, password)) log.Debug(o) log.Infof("Password for user %s was changed", username) return nil, "Password changed" } return errors.New(fmt.Sprintf("User \"%s\" not found}", username)), fmt.Sprintf("{\"msg\":\"User \"%s\" not found\"}", username) } func (oAdmin *OvpnAdmin) getUserStatistic(username string) []clientStatus { var userStatistic []clientStatus for _, u := range oAdmin.activeClients { if u.CommonName == username { userStatistic = append(userStatistic, u) } } return userStatistic } func (oAdmin *OvpnAdmin) userRevoke(username string) (error, string) { log.Infof("Revoke certificate for user %s", username) if checkUserExist(username) { // check certificate valid flag 'V' if *storageBackend == "kubernetes.secrets" { err := app.easyrsaRevoke(username) if err != nil { log.Error(err) } } else { o := runBash(fmt.Sprintf("cd %[1]s && echo yes | %[2]s revoke %[3]s 1>/dev/null && %[2]s gen-crl 1>/dev/null", *easyrsaDirPath, *easyrsaBinPath, username)) log.Debugln(o) } if *authByPassword { o := runBash(fmt.Sprintf("openvpn-user revoke --db-path %s --user %s", *authDatabase, username)) log.Debug(o) } crlFix() userConnected, userConnectedTo := isUserConnected(username, oAdmin.activeClients) log.Tracef("User %s connected: %t", username, userConnected) if userConnected { for _, connection := range userConnectedTo { oAdmin.mgmtKillUserConnection(username, connection) log.Infof("Session for user \"%s\" killed", username) } } oAdmin.setState() return nil, fmt.Sprintf("user \"%s\" revoked", username) } log.Infof("user \"%s\" not found", username) return errors.New(fmt.Sprintf("User \"%s\" not found}", username)), fmt.Sprintf("User \"%s\" not found", username) } func (oAdmin *OvpnAdmin) userUnrevoke(username string) (error, string) { if checkUserExist(username) { if *storageBackend == "kubernetes.secrets" { err := app.easyrsaUnrevoke(username) if err != nil { log.Error(err) } } else { // check certificate revoked flag 'R' usersFromIndexTxt := indexTxtParser(fRead(*indexTxtPath)) for i := range usersFromIndexTxt { if usersFromIndexTxt[i].DistinguishedName == "/CN="+username { if usersFromIndexTxt[i].Flag == "R" { usersFromIndexTxt[i].Flag = "V" usersFromIndexTxt[i].RevocationDate = "" err := fMove(fmt.Sprintf("%s/pki/revoked/certs_by_serial/%s.crt", *easyrsaDirPath, usersFromIndexTxt[i].SerialNumber), fmt.Sprintf("%s/pki/issued/%s.crt", *easyrsaDirPath, username)) if err != nil { log.Error(err) } err = fMove(fmt.Sprintf("%s/pki/revoked/certs_by_serial/%s.crt", *easyrsaDirPath, usersFromIndexTxt[i].SerialNumber), fmt.Sprintf("%s/pki/certs_by_serial/%s.pem", *easyrsaDirPath, usersFromIndexTxt[i].SerialNumber)) if err != nil { log.Error(err) } err = fMove(fmt.Sprintf("%s/pki/revoked/private_by_serial/%s.key", *easyrsaDirPath, usersFromIndexTxt[i].SerialNumber), fmt.Sprintf("%s/pki/private/%s.key", *easyrsaDirPath, username)) if err != nil { log.Error(err) } err = fMove(fmt.Sprintf("%s/pki/revoked/reqs_by_serial/%s.req", *easyrsaDirPath, usersFromIndexTxt[i].SerialNumber), fmt.Sprintf("%s/pki/reqs/%s.req", *easyrsaDirPath, username)) if err != nil { log.Error(err) } err = fWrite(*indexTxtPath, renderIndexTxt(usersFromIndexTxt)) if err != nil { log.Error(err) } _ = runBash(fmt.Sprintf("cd %s && %s gen-crl 1>/dev/null", *easyrsaDirPath, *easyrsaBinPath)) if *authByPassword { o := runBash(fmt.Sprintf("openvpn-user restore --db-path %s --user %s", *authDatabase, username)) log.Debug(o) } crlFix() break } } } err := fWrite(*indexTxtPath, renderIndexTxt(usersFromIndexTxt)) if err != nil { log.Error(err) } //fmt.Print(renderIndexTxt(usersFromIndexTxt)) } crlFix() oAdmin.clients = oAdmin.usersList() return nil, fmt.Sprintf("{\"msg\":\"User %s successfully unrevoked\"}", username) } return errors.New(fmt.Sprintf("user \"%s\" not found", username)), fmt.Sprintf("{\"msg\":\"User \"%s\" not found\"}", username) } func (oAdmin *OvpnAdmin) userRotate(username, newPassword string) (error, string) { if checkUserExist(username) { if *storageBackend == "kubernetes.secrets" { err := app.easyrsaRotate(username, newPassword) if err != nil { log.Error(err) } } else { var oldUserIndex, newUserIndex int var oldUserSerial string uniqHash := strings.Replace(uuid.New().String(), "-", "", -1) usersFromIndexTxt := indexTxtParser(fRead(*indexTxtPath)) for i := range usersFromIndexTxt { if usersFromIndexTxt[i].DistinguishedName == "/CN="+username { oldUserSerial = usersFromIndexTxt[i].SerialNumber usersFromIndexTxt[i].DistinguishedName = "/CN=REVOKED-" + username + "-" + uniqHash oldUserIndex = i break } } err := fWrite(*indexTxtPath, renderIndexTxt(usersFromIndexTxt)) if err != nil { log.Error(err) } if *authByPassword { o := runBash(fmt.Sprintf("openvpn-user delete --force --db.path %s --user %s", *authDatabase, username)) log.Debug(o) } userCreated, userCreateMessage := oAdmin.userCreate(username, newPassword) if !userCreated { usersFromIndexTxt = indexTxtParser(fRead(*indexTxtPath)) for i := range usersFromIndexTxt { if usersFromIndexTxt[i].SerialNumber == oldUserSerial { usersFromIndexTxt[i].DistinguishedName = "/CN=" + username break } } err = fWrite(*indexTxtPath, renderIndexTxt(usersFromIndexTxt)) if err != nil { log.Error(err) } return errors.New(fmt.Sprintf("error rotaing user due: %s", userCreateMessage)), userCreateMessage } usersFromIndexTxt = indexTxtParser(fRead(*indexTxtPath)) for i := range usersFromIndexTxt { if usersFromIndexTxt[i].DistinguishedName == "/CN="+username { newUserIndex = i } if usersFromIndexTxt[i].SerialNumber == oldUserSerial { oldUserIndex = i } } usersFromIndexTxt[oldUserIndex], usersFromIndexTxt[newUserIndex] = usersFromIndexTxt[newUserIndex], usersFromIndexTxt[oldUserIndex] err = fWrite(*indexTxtPath, renderIndexTxt(usersFromIndexTxt)) if err != nil { log.Error(err) } _ = runBash(fmt.Sprintf("cd %s && %s gen-crl 1>/dev/null", *easyrsaDirPath, *easyrsaBinPath)) } crlFix() oAdmin.clients = oAdmin.usersList() return nil, fmt.Sprintf("{\"msg\":\"User %s successfully rotated\"}", username) } return errors.New(fmt.Sprintf("user \"%s\" not found", username)), fmt.Sprintf("{\"msg\":\"User \"%s\" not found\"}", username) } func (oAdmin *OvpnAdmin) userDelete(username string) (error, string) { if checkUserExist(username) { if *storageBackend == "kubernetes.secrets" { err := app.easyrsaDelete(username) if err != nil { log.Error(err) } } else { uniqHash := strings.Replace(uuid.New().String(), "-", "", -1) usersFromIndexTxt := indexTxtParser(fRead(*indexTxtPath)) for i := range usersFromIndexTxt { if usersFromIndexTxt[i].DistinguishedName == "/CN="+username { usersFromIndexTxt[i].DistinguishedName = "/CN=REVOKED-" + username + "-" + uniqHash break } } if *authByPassword { _ = runBash(fmt.Sprintf("openvpn-user delete --force --db.path %s --user %s", *authDatabase, username)) } err := fWrite(*indexTxtPath, renderIndexTxt(usersFromIndexTxt)) if err != nil { log.Error(err) } _ = runBash(fmt.Sprintf("cd %s && %s gen-crl 1>/dev/null ", *easyrsaDirPath, *easyrsaBinPath)) } crlFix() oAdmin.clients = oAdmin.usersList() return nil, fmt.Sprintf("{\"msg\":\"User %s successfully deleted\"}", username) } return errors.New(fmt.Sprintf("User \"%s\" not found}", username)), fmt.Sprintf("{\"msg\":\"User \"%s\" not found\"}", username) } func (oAdmin *OvpnAdmin) mgmtRead(conn net.Conn) string { recvData := make([]byte, 32768) var out string var n int var err error for { n, err = conn.Read(recvData) if n <= 0 || err != nil { break } else { out += string(recvData[:n]) if strings.Contains(out, "type 'help' for more info") || strings.Contains(out, "END") || strings.Contains(out, "SUCCESS:") || strings.Contains(out, "ERROR:") { break } } } return out } func (oAdmin *OvpnAdmin) mgmtConnectedUsersParser(text, serverName string) []clientStatus { var u []clientStatus isClientList := false isRouteTable := false scanner := bufio.NewScanner(strings.NewReader(text)) for scanner.Scan() { txt := scanner.Text() if regexp.MustCompile(`^Common Name,Real Address,Bytes Received,Bytes Sent,Connected Since$`).MatchString(txt) { isClientList = true continue } if regexp.MustCompile(`^ROUTING TABLE$`).MatchString(txt) { isClientList = false continue } if regexp.MustCompile(`^Virtual Address,Common Name,Real Address,Last Ref$`).MatchString(txt) { isRouteTable = true continue } if regexp.MustCompile(`^GLOBAL STATS$`).MatchString(txt) { // isRouteTable = false // ineffectual assignment to isRouteTable (ineffassign) break } if isClientList { user := strings.Split(txt, ",") userName := user[0] userAddress := user[1] userBytesReceived := user[2] userBytesSent := user[3] userConnectedSince := user[4] userStatus := clientStatus{CommonName: userName, RealAddress: userAddress, BytesReceived: userBytesReceived, BytesSent: userBytesSent, ConnectedSince: userConnectedSince, ConnectedTo: serverName} u = append(u, userStatus) bytesSent, _ := strconv.Atoi(userBytesSent) bytesReceive, _ := strconv.Atoi(userBytesReceived) ovpnClientConnectionFrom.WithLabelValues(userName, userAddress).Set(float64(parseDateToUnix(oAdmin.mgmtStatusTimeFormat, userConnectedSince))) ovpnClientBytesSent.WithLabelValues(userName).Set(float64(bytesSent)) ovpnClientBytesReceived.WithLabelValues(userName).Set(float64(bytesReceive)) } if isRouteTable { user := strings.Split(txt, ",") for i := range u { if u[i].CommonName == user[1] { u[i].VirtualAddress = user[0] u[i].LastRef = user[3] ovpnClientConnectionInfo.WithLabelValues(user[1], user[0]).Set(float64(parseDateToUnix(oAdmin.mgmtStatusTimeFormat, user[3]))) break } } } } return u } func (oAdmin *OvpnAdmin) mgmtKillUserConnection(username, serverName string) { conn, err := net.Dial("tcp", oAdmin.mgmtInterfaces[serverName]) if err != nil { log.Errorf("openvpn mgmt interface for %s is not reachable by addr %s", serverName, oAdmin.mgmtInterfaces[serverName]) return } oAdmin.mgmtRead(conn) // read welcome message conn.Write([]byte(fmt.Sprintf("kill %s\n", username))) fmt.Printf("%v", oAdmin.mgmtRead(conn)) conn.Close() } func (oAdmin *OvpnAdmin) mgmtGetActiveClients() []clientStatus { var activeClients []clientStatus for srv, addr := range oAdmin.mgmtInterfaces { conn, err := net.Dial("tcp", addr) if err != nil { log.Warnf("openvpn mgmt interface for %s is not reachable by addr %s", srv, addr) break } oAdmin.mgmtRead(conn) // read welcome message conn.Write([]byte("status\n")) activeClients = append(activeClients, oAdmin.mgmtConnectedUsersParser(oAdmin.mgmtRead(conn), srv)...) conn.Close() } return activeClients } func (oAdmin *OvpnAdmin) mgmtSetTimeFormat() { // time format for version 2.5 and may be newer oAdmin.mgmtStatusTimeFormat = "2006-01-02 15:04:05" log.Debugf("mgmtStatusTimeFormat: %s", oAdmin.mgmtStatusTimeFormat) type serverVersion struct { name string version string } var serverVersions []serverVersion for srv, addr := range oAdmin.mgmtInterfaces { var conn net.Conn var err error for connAttempt := 0; connAttempt < 10; connAttempt++ { conn, err = net.Dial("tcp", addr) if err == nil { log.Debugf("mgmtSetTimeFormat: successful connection to %s/%s", srv, addr) break } log.Warnf("mgmtSetTimeFormat: openvpn mgmt interface for %s is not reachable by addr %s", srv, addr) time.Sleep(time.Duration(2) * time.Second) } if err != nil { break } oAdmin.mgmtRead(conn) // read welcome message conn.Write([]byte("version\n")) out := oAdmin.mgmtRead(conn) conn.Close() log.Trace(out) for _, s := range strings.Split(out, "\n") { if strings.Contains(s, "OpenVPN Version:") { serverVersions = append(serverVersions, serverVersion{srv, strings.Split(s, " ")[3]}) break } } } if len(serverVersions) == 0 { return } firstVersion := serverVersions[0].version if strings.HasPrefix(firstVersion, "2.4") { oAdmin.mgmtStatusTimeFormat = time.ANSIC log.Debugf("mgmtStatusTimeFormat changed: %s", oAdmin.mgmtStatusTimeFormat) } warn := "" for _, v := range serverVersions { if firstVersion != v.version { warn = "mgmtSetTimeFormat: servers have different versions of openvpn, user connection status may not work" log.Warn(warn) break } } if warn != "" { for _, v := range serverVersions { log.Infof("server name: %s, version: %s", v.name, v.version) } } } func isUserConnected(username string, connectedUsers []clientStatus) (bool, []string) { var connections []string var connected = false for _, connectedUser := range connectedUsers { if connectedUser.CommonName == username { connected = true connections = append(connections, connectedUser.ConnectedTo) } } return connected, connections } func (oAdmin *OvpnAdmin) downloadCerts() bool { if fExist(certsArchivePath) { err := fDelete(certsArchivePath) if err != nil { log.Error(err) } } err := fDownload(certsArchivePath, *masterHost+*listenBaseUrl+downloadCertsApiUrl+"?token="+oAdmin.masterSyncToken, oAdmin.masterHostBasicAuth) if err != nil { log.Error(err) return false } return true } func (oAdmin *OvpnAdmin) downloadCcd() bool { if fExist(ccdArchivePath) { err := fDelete(ccdArchivePath) if err != nil { log.Error(err) } } err := fDownload(ccdArchivePath, *masterHost+*listenBaseUrl+downloadCcdApiUrl+"?token="+oAdmin.masterSyncToken, oAdmin.masterHostBasicAuth) if err != nil { log.Error(err) return false } return true } func archiveCerts() { err := createArchiveFromDir(*easyrsaDirPath+"/pki", certsArchivePath) if err != nil { log.Warnf("archiveCerts(): %s", err) } } func archiveCcd() { err := createArchiveFromDir(*ccdDir, ccdArchivePath) if err != nil { log.Warnf("archiveCcd(): %s", err) } } func unArchiveCerts() { if err := os.MkdirAll(*easyrsaDirPath+"/pki", 0755); err != nil { log.Warnf("unArchiveCerts(): error creating pki dir: %s", err) } err := extractFromArchive(certsArchivePath, *easyrsaDirPath+"/pki") if err != nil { log.Warnf("unArchiveCerts: extractFromArchive() %s", err) } } func unArchiveCcd() { if err := os.MkdirAll(*ccdDir, 0755); err != nil { log.Warnf("unArchiveCcd(): error creating ccd dir: %s", err) } err := extractFromArchive(ccdArchivePath, *ccdDir) if err != nil { log.Warnf("unArchiveCcd: extractFromArchive() %s", err) } } func (oAdmin *OvpnAdmin) syncDataFromMaster() { retryCountMax := 3 certsDownloadFailed := true ccdDownloadFailed := true for certsDownloadRetries := 0; certsDownloadRetries < retryCountMax; certsDownloadRetries++ { log.Infof("Downloading archive with certificates from master. Attempt %d", certsDownloadRetries) if oAdmin.downloadCerts() { certsDownloadFailed = false log.Info("Decompressing archive with certificates from master") unArchiveCerts() log.Info("Decompression archive with certificates from master completed") break } else { log.Warnf("Something goes wrong during downloading archive with certificates from master. Attempt %d", certsDownloadRetries) } } for ccdDownloadRetries := 0; ccdDownloadRetries < retryCountMax; ccdDownloadRetries++ { log.Infof("Downloading archive with ccd from master. Attempt %d", ccdDownloadRetries) if oAdmin.downloadCcd() { ccdDownloadFailed = false log.Info("Decompressing archive with ccd from master") unArchiveCcd() log.Info("Decompression archive with ccd from master completed") break } else { log.Warnf("Something goes wrong during downloading archive with ccd from master. Attempt %d", ccdDownloadRetries) } } oAdmin.lastSyncTime = time.Now().Format(stringDateFormat) if !ccdDownloadFailed && !certsDownloadFailed { oAdmin.lastSuccessfulSyncTime = time.Now().Format(stringDateFormat) } } func (oAdmin *OvpnAdmin) syncWithMaster() { for { time.Sleep(time.Duration(*masterSyncFrequency) * time.Second) oAdmin.syncDataFromMaster() } } func getOvpnServerHostsFromKubeApi() ([]OpenvpnServer, error) { var hosts []OpenvpnServer var lbHost string config, err := rest.InClusterConfig() if err != nil { log.Errorf("%s", err.Error()) } clientset, err := kubernetes.NewForConfig(config) if err != nil { log.Errorf("%s", err.Error()) } for _, serviceName := range *openvpnServiceName { service, err := clientset.CoreV1().Services(fRead(kubeNamespaceFilePath)).Get(context.TODO(), serviceName, metav1.GetOptions{}) if err != nil { log.Error(err) } log.Tracef("service from kube api %v", service) log.Tracef("service.Status from kube api %v", service.Status) log.Tracef("service.Status.LoadBalancer from kube api %v", service.Status.LoadBalancer) lbIngress := service.Status.LoadBalancer.Ingress if len(lbIngress) > 0 { if lbIngress[0].Hostname != "" { lbHost = lbIngress[0].Hostname } if lbIngress[0].IP != "" { lbHost = lbIngress[0].IP } } hosts = append(hosts, OpenvpnServer{lbHost, strconv.Itoa(int(service.Spec.Ports[0].Port)), strings.ToLower(string(service.Spec.Ports[0].Protocol))}) } if len(hosts) == 0 { return []OpenvpnServer{{Host: "kubernetes services not found"}}, err } return hosts, nil } func getOvpnCaCertExpireDate() time.Time { caCertPath := *easyrsaDirPath + "/pki/ca.crt" caCert, err := ioutil.ReadFile(caCertPath) if err != nil { log.Errorf("error read file %s: %s", caCertPath, err.Error()) } certPem, _ := pem.Decode(caCert) certPemBytes := certPem.Bytes cert, err := x509.ParseCertificate(certPemBytes) if err != nil { log.Errorf("error parse certificate ca.crt: %s", err.Error()) return time.Now() } return cert.NotAfter } // https://community.openvpn.net/openvpn/ticket/623 func crlFix() { err := os.Chmod(*easyrsaDirPath+"/pki", 0755) if err != nil { log.Error(err) } err = os.Chmod(*easyrsaDirPath+"/pki/crl.pem", 0644) if err != nil { log.Error(err) } }