8fc518dba8
This pull request corrects the regular expression used for username validation to correctly recognize hyphens (-). Changes Made: Changed the regex pattern from ^([a-zA-Z0-9_.-@])+$ to ^([a-zA-Z0-9_.\-@])+$. Reason for Change: In the previous regex, the hyphen (-) within the character class was interpreted as a range operator, not as a literal character. This caused usernames with hyphens to be incorrectly marked as invalid. By escaping the hyphen (\-), the regex now correctly recognizes it as a literal character. This ensures that usernames containing hyphens are validated properly. Points of Verification: Confirmed that usernames containing hyphens are now correctly recognized and pass the validation. Verified that other characters (letters, numbers, underscores, dots, and at signs) are still being properly validated.
1622 lines
51 KiB
Go
1622 lines
51 KiB
Go
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)
|
|
}
|
|
}
|