Add passwd auth feature; Fixes; Some layout changes

This commit is contained in:
Ilya Sosnovsky 2021-02-15 09:03:38 +03:00
parent 0af5fc3622
commit bec0e738d1
12 changed files with 326 additions and 88 deletions

View File

@ -16,3 +16,6 @@ frontend/node_modules
openvpn-web-ui
openvpn-ui
openvpn-admin
docker-compose.yaml
docker-compose-slave.yaml

16
.werffiles/auth.sh Normal file
View File

@ -0,0 +1,16 @@
#!/usr/bin/env sh
PATH=$PATH:/usr/local/bin
set -e
env
auth_usr=$(head -1 $1)
auth_passwd=$(tail -1 $1)
if [ $common_name = $username ]; then
openvpn-user auth --db.path /etc/openvpn/easyrsa/pki/users.db --user ${auth_usr} --password ${auth_passwd}
else
echo "Authorization failed"
exit 1
fi

View File

@ -1,5 +1,5 @@
#!/usr/bin/env bash
set -x
set -ex
EASY_RSA_LOC="/etc/openvpn/easyrsa"
SERVER_CERT="${EASY_RSA_LOC}/pki/issued/server.crt"
@ -34,10 +34,20 @@ fi
cp -f /etc/openvpn/setup/openvpn.conf /etc/openvpn/openvpn.conf
if [ ${OPVN_PASSWD_AUTH} = "true" ]; then
mkdir -p /etc/openvpn/scripts/
cp -f /etc/openvpn/setup/auth.sh /etc/openvpn/scripts/auth.sh
chmod +x /etc/openvpn/scripts/auth.sh
echo "auth-user-pass-verify /etc/openvpn/scripts/auth.sh via-file" | tee -a /etc/openvpn/openvpn.conf
echo "script-security 2" | tee -a /etc/openvpn/openvpn.conf
echo "verify-client-cert require" | tee -a /etc/openvpn/openvpn.conf
openvpn-user db-init --db.path=$EASY_RSA_LOC/pki/users.db
fi
[ -d $EASY_RSA_LOC/pki ] && chmod 755 $EASY_RSA_LOC/pki
[ -f $EASY_RSA_LOC/pki/crl.pem ] && chmod 644 $EASY_RSA_LOC/pki/crl.pem
mkdir -p /etc/openvpn/ccd
openvpn --config /etc/openvpn/openvpn.conf --client-config-dir /etc/openvpn/ccd
openvpn --config /etc/openvpn/openvpn.conf --client-config-dir /etc/openvpn/ccd --port 1194 --proto tcp --management 127.0.0.1 8989

View File

@ -8,15 +8,14 @@ dh /etc/openvpn/easyrsa/pki/dh.pem
crl-verify /etc/openvpn/easyrsa/pki/crl.pem
tls-auth /etc/openvpn/easyrsa/pki/ta.key
key-direction 0
duplicate-cn
cipher AES-128-CBC
management 127.0.0.1 8989
#management 127.0.0.1 8989
keepalive 10 60
persist-key
persist-tun
topology subnet
proto tcp
port 1194
#proto tcp
#port 1194
dev tun0
status /tmp/openvpn-status.log
user nobody

View File

@ -7,13 +7,16 @@ FROM node:14.2-alpine3.11 AS frontend-builder
COPY frontend/ /app
RUN cd /app && npm install && npm run build
FROM alpine:3.11
FROM golang:1.14.2-buster AS user-builder
RUN git clone https://github.com/pashcovich/openvpn-user /app && cd /app && env CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -ldflags='-linkmode external -extldflags "-static" -s -w' -o openvpn-user
FROM alpine:3.13
WORKDIR /app
COPY --from=backend-builder /app/openvpn-admin /app
COPY --from=user-builder /app/openvpn-user /usr/local/bin
COPY --from=frontend-builder /app/static /app/static
COPY client.conf.tpl /app/client.conf.tpl
COPY ccd.tpl /app/ccd.tpl
RUN echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing/" >> /etc/apk/repositories && \
apk add --update bash easy-rsa && \
RUN apk add --update bash easy-rsa && \
ln -s /usr/share/easy-rsa/easyrsa /usr/local/bin && \
rm -rf /tmp/* /var/tmp/* /var/cache/apk/* /var/cache/distfiles/*

View File

@ -1,6 +1,9 @@
FROM alpine:3.11
RUN echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing/" >> /etc/apk/repositories && \
apk add --update bash openvpn easy-rsa && \
FROM golang:1.14.2-buster AS user-builder
RUN git clone https://github.com/pashcovich/openvpn-user /app && cd /app && env CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -ldflags='-linkmode external -extldflags "-static" -s -w' -o openvpn-user
FROM alpine:3.13
COPY --from=user-builder /app/openvpn-user /usr/local/bin
RUN apk add --update bash openvpn easy-rsa && \
ln -s /usr/share/easy-rsa/easyrsa /usr/local/bin && \
rm -rf /tmp/* /var/tmp/* /var/cache/apk/* /var/cache/distfiles/*
COPY .werffiles /etc/openvpn/setup

View File

@ -11,10 +11,19 @@ key-direction 1
#redirect-gateway def1
tls-client
remote-cert-tls server
# for update resolv.conf on ubuntu
#script-security 2 system
# uncomment needed below lines for use with linux
#script-security 2
# if use use resolved
#up /etc/openvpn/update-resolv-conf
#down /etc/openvpn/update-resolv-conf
# if you use systemd-resolved first install and openvpn-systemd-resolved package
#up /etc/openvpn/update-systemd-resolved
#down /etc/openvpn/update-systemd-resolved
{{- if .PasswdAuth }}
auth-user-pass
{{- end }}
<cert>
{{ .Cert -}}
</cert>

View File

@ -8,6 +8,7 @@ services:
image: openvpn:local
command: /etc/openvpn/setup/configure.sh
environment:
- OPVN_PASSWD_AUTH=true
- OPVN_ROLE=slave
cap_add:
- NET_ADMIN
@ -21,7 +22,7 @@ services:
build:
context: .
image: openvpn-admin:local
command: /app/openvpn-admin --debug --ovpn.network="172.16.100.0/22" --master.sync-token="TOKEN" --master.host="http://172.20.0.1:8080" --role="slave" --ovpn.host="127.0.0.1:7744" --ovpn.host="127.0.0.1:7778"
command: /app/openvpn-admin --debug --ovpn.network="172.16.100.0/22" --master.sync-token="TOKEN" --master.host="http://172.20.0.1:8080" --role="slave" --ovpn.host="127.0.0.1:7744" --ovpn.host="127.0.0.1:7778" --auth.password
environment:
- OPVN_SLAVE=1
network_mode: service:openvpn

View File

@ -7,6 +7,8 @@ services:
dockerfile: Dockerfile.openvpn
image: openvpn:local
command: /etc/openvpn/setup/configure.sh
environment:
- OPVN_PASSWD_AUTH=true
cap_add:
- NET_ADMIN
ports:
@ -19,7 +21,7 @@ services:
build:
context: .
image: openvpn-admin:local
command: /app/openvpn-admin --debug --ovpn.network="172.16.100.0/22" --master.sync-token="TOKEN"
command: /app/openvpn-admin --debug --ovpn.network="172.16.100.0/22" --master.sync-token="TOKEN" --auth.password
network_mode: service:openvpn
volumes:
- ./easyrsa_master:/mnt/easyrsa

View File

@ -54,6 +54,11 @@ new Vue({
field: 'AccountStatus',
filterable: true,
},
{
label: 'Connection Server',
field: 'ConnectionServer',
filterable: true,
},
{
label: 'Expiration Date',
field: 'ExpirationDate',
@ -84,39 +89,52 @@ new Vue({
],
rows: [],
actions: [
{
name: 'u-change-password',
label: 'Change password',
class: 'btn-warning',
showWhenStatus: 'Active',
showForServerRole: ['master']
},
{
name: 'u-revoke',
label: 'Revoke',
class: 'btn-warning',
showWhenStatus: 'Active',
showForServerRole: ['master']
},
{
name: 'u-unrevoke',
label: 'Unrevoke',
class: 'btn-primary',
showWhenStatus: 'Revoked',
showForServerRole: ['master']
},
{
name: 'u-show-config',
label: 'Show config',
showWhenStatus: 'Active',
showForServerRole: ['master', 'slave']
},
// {
// name: 'u-show-config',
// label: 'Show config',
// class: 'btn-primary',
// showWhenStatus: 'Active',
// showForServerRole: ['master', 'slave']
// },
{
name: 'u-download-config',
label: 'Download config',
class: 'btn-info',
showWhenStatus: 'Active',
showForServerRole: ['master', 'slave']
},
{
name: 'u-edit-ccd',
label: 'Edit routes',
class: 'btn-primary',
showWhenStatus: 'Active',
showForServerRole: ['master']
},
{
name: 'u-edit-ccd',
label: 'Show routes',
class: 'btn-primary',
showWhenStatus: 'Active',
showForServerRole: ['slave']
}
@ -128,11 +146,15 @@ new Vue({
lastSync: "unknown",
u: {
newUserName: '',
// newUserPassword: 'nopass',
newUserPassword: '',
newUserCreateError: '',
newPassword: '',
passwordChangeStatus: '',
passwordChangeMessage: '',
modalNewUserVisible: false,
modalShowConfigVisible: false,
modalShowCcdVisible: false,
modalChangePasswordVisible: false,
openvpnConfig: '',
ccd: {
Name: '',
@ -210,6 +232,11 @@ new Vue({
console.log(response.data);
});
})
_this.$root.$on('u-change-password', function () {
_this.u.modalChangePasswordVisible = true;
var data = new URLSearchParams();
data.append('username', _this.username);
})
},
computed: {
customAddressDisabled: function () {
@ -218,6 +245,9 @@ new Vue({
ccdApplyStatusCssClass: function () {
return this.u.ccdApplyStatus == 200 ? "alert-success" : "alert-danger"
},
passwordChangeStatusCssClass: function () {
return this.u.passwordChangeStatus == 200 ? "alert-success" : "alert-danger"
},
modalNewUserDisplay: function () {
return this.u.modalNewUserVisible ? {display: 'flex'} : {}
},
@ -227,6 +257,9 @@ new Vue({
modalShowCcdDisplay: function () {
return this.u.modalShowCcdVisible ? {display: 'flex'} : {}
},
modalChangePasswordDisplay: function () {
return this.u.modalChangePasswordVisible ? {display: 'flex'} : {}
},
revokeFilterText: function() {
return this.filters.hideRevoked ? "Show revoked" : "Hide revoked"
},
@ -269,6 +302,7 @@ new Vue({
}
});
},
createUser: function() {
var _this = this;
@ -276,19 +310,20 @@ new Vue({
var data = new URLSearchParams();
data.append('username', _this.u.newUserName);
// data.append('password', this.u.newUserPassword);
data.append('password', _this.u.newUserPassword);
axios.request(axios_cfg('api/user/create', data, 'form'))
.then(function(response) {
_this.getUserData();
_this.u.modalNewUserVisible = false;
_this.u.newUserName = '';
// _this.u.newUserPassword = 'nopass';
_this.u.newUserPassword = '';
_this.getUserData();
})
.catch(function(error) {
_this.u.newUserCreateError = error.response.data;
});
},
ccdApply: function() {
var _this = this;
@ -304,6 +339,30 @@ new Vue({
_this.u.ccdApplyStatus = error.response.status;
_this.u.ccdApplyStatusMessage = error.response.data;
});
}
},
changeUserPassword: function(user) {
var _this = this;
_this.u.passwordChangeMessage = "";
var data = new URLSearchParams();
data.append('username', user);
data.append('password', _this.u.newPassword);
axios.request(axios_cfg('api/user/change-password', data, 'form'))
.then(function(response) {
_this.u.passwordChangeStatus = 200;
_this.u.newPassword = '';
_this.u.passwordChangeMessage = response.data.message;
_this.getUserData();
_this.u.modalChangePasswordVisible = false;
})
.catch(function(error) {
_this.u.passwordChangeStatus = error.response.status;
_this.u.passwordChangeMessage = error.response.data.message;
});
},
}
})

View File

@ -28,7 +28,7 @@
<template slot="table-row" slot-scope="props">
<span v-if="props.column.field == 'actions'">
<button
class="btn btn-sm btn-success el-square modal-el-margin"
class="btn btn-sm el-square modal-el-margin"
type="button"
:title="action.label"
:data-username="props.row.Identity"
@ -36,6 +36,7 @@
:data-text="action.label"
@click.left.stop="rowActionFn"
v-for="action in actions"
v-bind:class="action.class"
v-if="action.showWhenStatus == props.row.AccountStatus && action.showForServerRole.includes(serverRole)">
{{ action.label }}
</button>
@ -51,11 +52,11 @@
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4>Add new user </h4>
<h4>Add new user</h4>
</div>
<div class="modal-body">
<input type="text" class="form-control el-square modal-el-margin" placeholder="Username [_a-zA-Z0-9\.-]" v-model="u.newUserName">
<!-- <input type="password" class="form-control el-square modal-el-margin" minlength="6" autocomplete="off" placeholder="Password [_a-zA-Z0-9\.-]" v-model="u.newUserPassword">-->
<input type="password" class="form-control el-square modal-el-margin" minlength="6" autocomplete="off" placeholder="Password [_a-zA-Z0-9\.-]" v-model="u.newUserPassword">
</div>
<div class="modal-footer justify-content-center" v-if="u.newUserCreateError.length > 0">
@ -64,18 +65,41 @@
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success el-square modal-el-margin" v-on:click.stop="createUser();">Create</button>
<button type="button" class="btn btn-success el-square modal-el-margin" v-on:click.stop="createUser()">Create</button>
<button type="button" class="btn btn-primary el-square d-flex justify-content-sm-end modal-el-margin" v-on:click.stop="u.newUserName='';u.newUserPassword='nopass';u.modalNewUserVisible=false">Close</button>
</div>
</div>
</div>
</div>
<div class="modal-wrapper" v-if="u.modalChangePasswordVisible" v-bind:style="modalChangePasswordDisplay">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4>Change password for: <strong>{{ username }}</strong></h4>
</div>
<div class="modal-body">
<input type="password" class="form-control el-square modal-el-margin" minlength="6" autocomplete="off" placeholder="Password [_a-zA-Z0-9\.-]" v-model="u.newPassword">
</div>
<div class="modal-footer justify-content-center" v-if="u.passwordChangeMessage.length > 0">
<div class="alert" v-bind:class="passwordChangeStatusCssClass" role="alert" >
{{ u.passwordChangeMessage }}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success el-square modal-el-margin" v-on:click.stop="changeUserPassword(username)">Change password</button>
<button type="button" class="btn btn-primary el-square d-flex justify-content-sm-end modal-el-margin" v-on:click.stop="u.newPassword='';u.modalChangePasswordVisible=false">Close</button>
</div>
</div>
</div>
</div>
<div class="modal-wrapper" v-if="u.modalShowConfigVisible" v-bind:style="modalShowConfigDisplay">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4>ovpn config for {{ username }}</h4>
<h4>ovpn config for: <strong>{{ username }}</strong></h4>
</div>
<div class="modal-body">
<div class="d-flex">
@ -96,16 +120,18 @@
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h3 class="static-address-label ">Routes table for: <strong>{{ username }}</strong></h3>
</div>
<div class="modal-body">
<div class="input-group">
<h4 class="static-address-label ">Client "{{ username }}" static address</h4>
<h5 class="static-address-label ">Static address:</h5>
<div class="input-group-prepend">
<div class="input-group-text">
<input id="enable-static" type="checkbox" onchange="document.getElementById('staticAddress').disabled=!this.checked;" v-if="serverRole == 'master'" v-bind:checked="customAddressDisabled">
<input id="enable-static" type="checkbox" onchange="document.getElementById('staticAddress').disabled=!this.checked;" v-if="serverRole == 'master'" v-bind:checked="!customAddressDisabled">
</div>
</div>
<input id="staticAddress" type="text" class="form-control" v-model="u.ccd.ClientAddress" placeholder="127.0.0.1" v-bind:disabled="customAddressDisabled">
</div>
</div>
<div class="modal-body">
<div class="d-flex ">

211
main.go
View File

@ -20,7 +20,9 @@ import (
)
const (
usernameRegexp = `^([a-zA-Z0-9_.-])+$`
usernameRegexp = `^([a-zA-Z0-9_.-@])+$`
passwordRegexp = `^([a-zA-Z0-9_.-@])+$`
passwordMinLength = 6
downloadCertsApiUrl = "/api/data/certs/download"
downloadCcdApiUrl = "/api/data/ccd/download"
certsArchiveFileName = "certs.tar.gz"
@ -42,14 +44,17 @@ var (
masterSyncToken = kingpin.Flag("master.sync-token", "master host data sync security token").Default("justasimpleword").PlaceHolder("TOKEN").String()
openvpnServer = kingpin.Flag("ovpn.host","host(s) for openvpn server").Default("127.0.0.1:7777").PlaceHolder("HOST:PORT").Strings()
openvpnNetwork = kingpin.Flag("ovpn.network","network for openvpn server").Default("172.16.100.0/24").String()
mgmtListenHost = kingpin.Flag("mgmt.host","host for openvpn server mgmt interface").Default("127.0.0.1").String()
mgmtListenPort = kingpin.Flag("mgmt.port","port for openvpn server mgmt interface").Default("8989").String()
mgmtAddress = kingpin.Flag("mgmt","comma separated (alias=addresses) for openvpn servers mgmt interfaces").Default("main=127.0.0.1:8989").Strings()
//mgmtListenPort = kingpin.Flag("mgmt.port","port for openvpn server mgmt interface").Default("8989").String()
metricsPath = kingpin.Flag("metrics.path", "URL path for surfacing collected metrics").Default("/metrics").String()
easyrsaDirPath = kingpin.Flag("easyrsa.path", "path to easyrsa dir").Default("/mnt/easyrsa").String()
indexTxtPath = kingpin.Flag("easyrsa.index-path", "path to easyrsa index file.").Default("/mnt/easyrsa/pki/index.txt").String()
ccdDir = kingpin.Flag("ccd.path", "path to client-config-dir").Default("/mnt/ccd").String()
authByPassword = kingpin.Flag("auth.password", "Enable additional password authorization.").Default("false").Bool()
authDatabase = kingpin.Flag("auth.db", "Database path fort password authorization.").Default("/mnt/easyrsa/pki/users.db").String()
staticPath = kingpin.Flag("static.path", "path to static dir").Default("./static").String()
debug = kingpin.Flag("debug", "Enable debug mode.").Default("false").Bool()
verbose = kingpin.Flag("verbose", "Enable verbose mode.").Default("false").Bool()
certsArchivePath = "/tmp/" + certsArchiveFileName
ccdArchivePath = "/tmp/" + ccdArchiveFileName
@ -103,7 +108,7 @@ var (
ovpnClientConnectionInfo = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: "ovpn_client_connection_info",
Help: "openvpn user connection info. ip - assigned address from opvn network. value - last time when connection was refreshed in unix format",
Help: "openvpn user connection info. ip - assigned address from ovpn network. value - last time when connection was refreshed in unix format",
},
[]string{"client", "ip"},
)
@ -140,8 +145,10 @@ type OpenvpnAdmin struct {
clients []OpenvpnClient
activeClients []clientStatus
promRegistry *prometheus.Registry
mgmtInterfaces map[string]string
}
type OpenvpnServer struct {
Host string
Port string
@ -153,6 +160,7 @@ type openvpnClientConfig struct {
Cert string
Key string
TLS string
PasswdAuth bool
}
type OpenvpnClient struct {
@ -161,6 +169,7 @@ type OpenvpnClient struct {
ExpirationDate string `json:"ExpirationDate"`
RevocationDate string `json:"RevocationDate"`
ConnectionStatus string `json:"ConnectionStatus"`
ConnectionServer string `json:"ConnectionServer"`
}
type ccdRoute struct {
@ -186,15 +195,16 @@ type indexTxtLine struct {
}
type clientStatus struct {
CommonName string
RealAddress string
BytesReceived string
BytesSent string
ConnectedSince string
VirtualAddress string
LastRef string
CommonName string
RealAddress string
BytesReceived string
BytesSent string
ConnectedSince string
VirtualAddress string
LastRef string
ConnectedSinceFormatted string
LastRefFormatted string
ConnectedTo string
}
func (oAdmin *OpenvpnAdmin) userListHandler(w http.ResponseWriter, r *http.Request) {
@ -214,7 +224,7 @@ func (oAdmin *OpenvpnAdmin) userCreateHandler(w http.ResponseWriter, r *http.Req
return
}
r.ParseForm()
userCreated, userCreateStatus := oAdmin.userCreate(r.FormValue("username"))
userCreated, userCreateStatus := oAdmin.userCreate(r.FormValue("username"), r.FormValue("password"))
if userCreated {
w.WriteHeader(http.StatusOK)
@ -244,6 +254,25 @@ func (oAdmin *OpenvpnAdmin) userUnrevokeHandler(w http.ResponseWriter, r *http.R
fmt.Fprintf(w, "%s", oAdmin.userUnrevoke(r.FormValue("username")))
}
func (oAdmin *OpenvpnAdmin) userChangePasswordHandler(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
if *authByPassword {
passwordChanged, passwordChangeMessage := oAdmin.userChangePassword(r.FormValue("username"), r.FormValue("password"))
if passwordChanged {
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `{"status":"ok", "message": "%s"}`, passwordChangeMessage)
return
} else {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, `{"status":"error", "message": "%s"}`, passwordChangeMessage)
return
}
} else {
http.Error(w, `{"status":"error"}`, http.StatusNotImplemented )
}
}
func (oAdmin *OpenvpnAdmin) userShowConfigHandler(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
fmt.Fprintf(w, "%s", oAdmin.renderClientConfig(r.FormValue("username")))
@ -318,7 +347,7 @@ func (oAdmin *OpenvpnAdmin) downloadCertsHandler(w http.ResponseWriter, r *http.
http.ServeFile(w,r, certsArchivePath)
}
func (oAdmin *OpenvpnAdmin) downloadCddHandler(w http.ResponseWriter, r *http.Request) {
func (oAdmin *OpenvpnAdmin) downloadCcdHandler(w http.ResponseWriter, r *http.Request) {
if oAdmin.role == "slave" {
http.Error(w, `{"status":"error"}`, http.StatusLocked)
return
@ -346,6 +375,13 @@ func main() {
ovpnAdmin.masterSyncToken = *masterSyncToken
ovpnAdmin.promRegistry = prometheus.NewRegistry()
ovpnAdmin.mgmtInterfaces = make(map[string]string)
for _, mgmtInterface := range *mgmtAddress {
parts := strings.SplitN(mgmtInterface, "=",2)
ovpnAdmin.mgmtInterfaces[parts[0]] = parts[len(parts)-1]
}
ovpnAdmin.registerMetrics()
ovpnAdmin.setState()
@ -368,6 +404,7 @@ func main() {
http.HandleFunc("/api/server/role", ovpnAdmin.serverRoleHandler)
http.HandleFunc("/api/users/list", ovpnAdmin.userListHandler)
http.HandleFunc("/api/user/create", ovpnAdmin.userCreateHandler)
http.HandleFunc("/api/user/change-password", ovpnAdmin.userChangePasswordHandler)
http.HandleFunc("/api/user/revoke", ovpnAdmin.userRevokeHandler)
http.HandleFunc("/api/user/unrevoke", ovpnAdmin.userUnrevokeHandler)
http.HandleFunc("/api/user/config/show", ovpnAdmin.userShowConfigHandler)
@ -379,7 +416,7 @@ func main() {
http.HandleFunc("/api/sync/last/try", ovpnAdmin.lastSyncTimeHandler)
http.HandleFunc("/api/sync/last/successful", ovpnAdmin.lastSuccessfulSyncTimeHandler)
http.HandleFunc(downloadCertsApiUrl, ovpnAdmin.downloadCertsHandler)
http.HandleFunc(downloadCcdApiUrl, ovpnAdmin.downloadCddHandler)
http.HandleFunc(downloadCcdApiUrl, ovpnAdmin.downloadCcdHandler)
http.Handle(*metricsPath, promhttp.HandlerFor(ovpnAdmin.promRegistry, promhttp.HandlerOpts{}))
http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
@ -415,7 +452,7 @@ func (oAdmin *OpenvpnAdmin) setState() {
oAdmin.activeClients = oAdmin.mgmtGetActiveClients()
oAdmin.clients = oAdmin.usersList()
ovpnServerCaCertExpire.Set(float64((getOpvnCaCertExpireDate().Unix() - time.Now().Unix()) / 3600 / 24))
ovpnServerCaCertExpire.Set(float64((getOvpnCaCertExpireDate().Unix() - time.Now().Unix()) / 3600 / 24))
}
func (oAdmin *OpenvpnAdmin) updateState() {
@ -479,6 +516,7 @@ func (oAdmin *OpenvpnAdmin) renderClientConfig(username string) string {
conf.Cert = fRead(*easyrsaDirPath + "/pki/issued/" + username + ".crt")
conf.Key = fRead(*easyrsaDirPath + "/pki/private/" + username + ".key")
conf.TLS = fRead(*easyrsaDirPath + "/pki/ta.key")
conf.PasswdAuth = *authByPassword
t, _ := template.ParseFiles("client.conf.tpl")
var tmp bytes.Buffer
@ -624,6 +662,14 @@ func validateUsername(username string) bool {
return validUsername.MatchString(username)
}
func validatePassword(password string) bool {
if len(password) < passwordMinLength {
return false
} else {
return true
}
}
func checkUserExist(username string) bool {
for _, u := range indexTxtParser(fRead(*indexTxtPath)) {
if u.DistinguishedName == ("/CN=" + username) {
@ -663,8 +709,12 @@ func (oAdmin *OpenvpnAdmin) usersList() []OpenvpnClient {
expiredCerts += 1
}
if isUserConnected(line.Identity, oAdmin.activeClients) {
ovpnClient.ConnectionServer = ""
userConnected, userConnectedTo := isUserConnected(line.Identity, oAdmin.activeClients)
if userConnected {
ovpnClient.ConnectionStatus = "Connected"
ovpnClient.ConnectionServer = userConnectedTo
connectedUsers += 1
}
@ -689,32 +739,82 @@ func (oAdmin *OpenvpnAdmin) usersList() []OpenvpnClient {
return users
}
func (oAdmin *OpenvpnAdmin) userCreate(username string) (bool, string) {
func (oAdmin *OpenvpnAdmin) userCreate(username, password string) (bool, string) {
ucErr := fmt.Sprintf("User \"%s\" created", username)
// TODO: add password for user cert . priority=low
if validateUsername(username) == false {
ucErr = fmt.Sprintf("Username \"%s\" incorrect, you can use only %s\n", username, usernameRegexp)
if *debug {
log.Printf("ERROR: userCreate: %s", ucErr)
}
return false, ucErr
}
if checkUserExist(username) {
ucErr = fmt.Sprintf("User \"%s\" already exists\n", username)
if *debug {
log.Printf("ERROR: userCreate: %s", ucErr)
}
if *debug {
log.Printf("ERROR: userCreate: %s", ucErr)
}
return false, ucErr
}
if ! validateUsername(username) {
ucErr = fmt.Sprintf("Username \"%s\" incorrect, you can use only %s\n", username, usernameRegexp)
if *debug {
log.Printf("ERROR: userCreate: %s", ucErr)
}
return false, ucErr
}
if ! validatePassword(password) {
ucErr = fmt.Sprintf("Password too short, password length must be greater or equal %d", passwordMinLength)
if *debug {
log.Printf("ERROR: userCreate: %s\n", ucErr)
}
return false, ucErr
}
o := runBash(fmt.Sprintf("date +%%Y-%%m-%%d\\ %%H:%%M:%%S && cd %s && easyrsa build-client-full %s nopass", *easyrsaDirPath, username))
fmt.Println(o)
if *debug {
if *authByPassword {
o = runBash(fmt.Sprintf("openvpn-user create --db.path %s --user %s --password %s", *authDatabase, username, password))
fmt.Println(o)
}
if *verbose {
log.Printf("INFO: user created: %s", username)
}
oAdmin.clients = oAdmin.usersList()
return true, ucErr
}
func (oAdmin *OpenvpnAdmin) userChangePassword(username, password string) (bool, string) {
if checkUserExist(username) {
o := runBash(fmt.Sprintf("openvpn-user check --db.path %s --user %s | grep %s | wc -l", *authDatabase, username, username))
fmt.Println(o)
if ! validatePassword(password) {
ucpErr := fmt.Sprintf("Password for too short, password length must be greater or equal %d", passwordMinLength)
if *debug {
log.Printf("ERROR: userChangePassword: %s\n", ucpErr)
}
return false, ucpErr
}
if strings.TrimSpace(o) == "0" {
fmt.Println("Creating user in users.db")
o = runBash(fmt.Sprintf("openvpn-user create --db.path %s --user %s --password %s", *authDatabase, username, password))
fmt.Println(o)
}
o = runBash(fmt.Sprintf("openvpn-user change-password --db.path %s --user %s --password %s", *authDatabase, username, password))
fmt.Println(o)
if *verbose {
log.Printf("INFO: password for user %s was changed", username)
}
return true, "Password changed"
}
return false, "User does not exist"
}
func (oAdmin *OpenvpnAdmin) getUserStatistic(username string) clientStatus {
for _, u := range oAdmin.activeClients {
if u.CommonName == username {
@ -728,6 +828,10 @@ func (oAdmin *OpenvpnAdmin) userRevoke(username string) string {
if checkUserExist(username) {
// check certificate valid flag 'V'
o := runBash(fmt.Sprintf("date +%%Y-%%m-%%d\\ %%H:%%M:%%S && cd %s && echo yes | easyrsa revoke %s && easyrsa gen-crl", *easyrsaDirPath, username))
if *authByPassword {
o = runBash(fmt.Sprintf("openvpn-user revoke --db-path %s --user %s", *authDatabase, username))
//fmt.Println(o)
}
crlFix()
oAdmin.clients = oAdmin.usersList()
return fmt.Sprintln(o)
@ -757,6 +861,10 @@ func (oAdmin *OpenvpnAdmin) userUnrevoke(username string) string {
//fmt.Print(renderIndexTxt(usersFromIndexTxt))
o = runBash(fmt.Sprintf("cd %s && easyrsa gen-crl", *easyrsaDirPath))
//fmt.Println(o)
if *authByPassword {
o = runBash(fmt.Sprintf("openvpn-user restore --db-path %s --user %s", *authDatabase, username))
//fmt.Println(o)
}
crlFix()
o = ""
fmt.Println(o)
@ -773,11 +881,6 @@ func (oAdmin *OpenvpnAdmin) userUnrevoke(username string) string {
return fmt.Sprintf("{\"msg\":\"User \"%s\" not found\"}", username)
}
// TODO: add ability to change password for user cert . priority=low
func userChangePassword(username string, newPassword string) bool {
return false
}
func (oAdmin *OpenvpnAdmin) mgmtRead(conn net.Conn) string {
buf := make([]byte, 32768)
@ -786,7 +889,7 @@ func (oAdmin *OpenvpnAdmin) mgmtRead(conn net.Conn) string {
return s
}
func (oAdmin *OpenvpnAdmin) mgmtConnectedUsersParser(text string) []clientStatus {
func (oAdmin *OpenvpnAdmin) mgmtConnectedUsersParser(text, serverName string) []clientStatus {
var u []clientStatus
isClientList := false
isRouteTable := false
@ -814,14 +917,14 @@ func (oAdmin *OpenvpnAdmin) mgmtConnectedUsersParser(text string) []clientStatus
userName := user[0]
userAddress := user[1]
userBytesRecieved:= user[2]
userBytesReceived:= user[2]
userBytesSent:= user[3]
userConnectedSince := user[4]
userStatus := clientStatus{CommonName: userName, RealAddress: userAddress, BytesReceived: userBytesRecieved, BytesSent: userBytesSent, ConnectedSince: userConnectedSince}
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(userBytesRecieved)
bytesReceive, _ := strconv.Atoi(userBytesReceived)
ovpnClientConnectionFrom.WithLabelValues(userName, userAddress).Set(float64(parseDateToUnix(ovpnStatusDateLayout, userConnectedSince)))
ovpnClientBytesSent.WithLabelValues(userName).Set(float64(bytesSent))
ovpnClientBytesReceived.WithLabelValues(userName).Set(float64(bytesReceive))
@ -841,8 +944,8 @@ func (oAdmin *OpenvpnAdmin) mgmtConnectedUsersParser(text string) []clientStatus
return u
}
func (oAdmin *OpenvpnAdmin) mgmtKillUserConnection(username string) {
conn, err := net.Dial("tcp", *mgmtListenHost+":"+*mgmtListenPort)
func (oAdmin *OpenvpnAdmin) mgmtKillUserConnection(username, serverName string) {
conn, err := net.Dial("tcp", oAdmin.mgmtInterfaces[serverName])
if err != nil {
log.Println("ERROR: openvpn mgmt interface is not reachable")
return
@ -854,25 +957,29 @@ func (oAdmin *OpenvpnAdmin) mgmtKillUserConnection(username string) {
}
func (oAdmin *OpenvpnAdmin) mgmtGetActiveClients() []clientStatus {
conn, err := net.Dial("tcp", *mgmtListenHost+":"+*mgmtListenPort)
if err != nil {
log.Println("ERROR: openvpn mgmt interface is not reachable")
return []clientStatus{}
var activeClients []clientStatus
for srv, addr := range oAdmin.mgmtInterfaces {
conn, err := net.Dial("tcp", addr)
if err != nil {
log.Printf("ERROR: openvpn mgmt interface for %s is not reachable by addr %s\n", srv, addr)
//return []clientStatus{}
}
oAdmin.mgmtRead(conn) // read welcome message
conn.Write([]byte("status\n"))
activeClients = append(activeClients, oAdmin.mgmtConnectedUsersParser(oAdmin.mgmtRead(conn), srv)...)
conn.Close()
}
oAdmin.mgmtRead(conn) // read welcome message
conn.Write([]byte("status\n"))
activeClients := oAdmin.mgmtConnectedUsersParser(oAdmin.mgmtRead(conn))
conn.Close()
return activeClients
}
func isUserConnected(username string, connectedUsers []clientStatus) bool {
func isUserConnected(username string, connectedUsers []clientStatus) (bool, string) {
for _, connectedUser := range connectedUsers {
if connectedUser.CommonName == username {
return true
return true, connectedUser.ConnectedTo
}
}
return false
return false, ""
}
func (oAdmin *OpenvpnAdmin) downloadCerts() bool {
@ -968,7 +1075,7 @@ func (oAdmin *OpenvpnAdmin) syncWithMaster() {
}
}
func getOpvnCaCertExpireDate() time.Time {
func getOvpnCaCertExpireDate() time.Time {
caCertPath := *easyrsaDirPath + "/pki/ca.crt"
caCertExpireDate := runBash(fmt.Sprintf("openssl x509 -in %s -noout -enddate | awk -F \"=\" {'print $2'}", caCertPath))