add feature for rotate and delete users; fixes; refactoring
This commit is contained in:
parent
53119e17b2
commit
a0daf5b4d7
22 changed files with 1576 additions and 10099 deletions
|
@ -13,6 +13,7 @@ ccd_master
|
||||||
ccd_slave
|
ccd_slave
|
||||||
werf.yaml
|
werf.yaml
|
||||||
frontend/node_modules
|
frontend/node_modules
|
||||||
|
frontend/static/dist
|
||||||
openvpn-web-ui
|
openvpn-web-ui
|
||||||
openvpn-ui
|
openvpn-ui
|
||||||
openvpn-admin
|
openvpn-admin
|
||||||
|
|
2
.github/workflows/publish-latest.yaml
vendored
2
.github/workflows/publish-latest.yaml
vendored
|
@ -5,7 +5,7 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: build latest images for relase
|
name: build latest images for release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
|
|
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
|
@ -17,7 +17,7 @@ jobs:
|
||||||
- name: checkout code
|
- name: checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: build binaries
|
- name: build binaries
|
||||||
uses: wangyoucao577/go-release-action@v1.22
|
uses: wangyoucao577/go-release-action@v1.28
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
goversion: 1.17
|
goversion: 1.17
|
||||||
|
|
2
.github/workflows/release_arm.yaml
vendored
2
.github/workflows/release_arm.yaml
vendored
|
@ -17,7 +17,7 @@ jobs:
|
||||||
- name: checkout code
|
- name: checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: build binaries
|
- name: build binaries
|
||||||
uses: wangyoucao577/go-release-action@v1.22
|
uses: wangyoucao577/go-release-action@v1.28
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
goversion: 1.17
|
goversion: 1.17
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM node:16.13.0-alpine3.12 AS frontend-builder
|
FROM node:16-alpine3.15 AS frontend-builder
|
||||||
COPY frontend/ /app
|
COPY frontend/ /app
|
||||||
RUN cd /app && npm install && npm run build
|
RUN cd /app && npm install && npm run build
|
||||||
|
|
||||||
|
@ -8,10 +8,10 @@ COPY --from=frontend-builder /app/static /app/frontend/static
|
||||||
COPY . /app
|
COPY . /app
|
||||||
RUN cd /app && packr2 && env CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -a -tags netgo -ldflags '-linkmode external -extldflags -static -s -w' -o ovpn-admin && packr2 clean
|
RUN cd /app && packr2 && env CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -a -tags netgo -ldflags '-linkmode external -extldflags -static -s -w' -o ovpn-admin && packr2 clean
|
||||||
|
|
||||||
FROM alpine:3.14
|
FROM alpine:3.16
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=backend-builder /app/ovpn-admin /app
|
COPY --from=backend-builder /app/ovpn-admin /app
|
||||||
RUN apk add --update bash easy-rsa openssl openvpn && \
|
RUN apk add --update bash easy-rsa openssl openvpn coreutils && \
|
||||||
ln -s /usr/share/easy-rsa/easyrsa /usr/local/bin && \
|
ln -s /usr/share/easy-rsa/easyrsa /usr/local/bin && \
|
||||||
wget https://github.com/pashcovich/openvpn-user/releases/download/v1.0.3/openvpn-user-linux-amd64.tar.gz -O - | tar xz -C /usr/local/bin && \
|
wget https://github.com/pashcovich/openvpn-user/releases/download/v1.0.4/openvpn-user-linux-amd64.tar.gz -O - | tar xz -C /usr/local/bin && \
|
||||||
rm -rf /tmp/* /var/tmp/* /var/cache/apk/* /var/cache/distfiles/*
|
rm -rf /tmp/* /var/tmp/* /var/cache/apk/* /var/cache/distfiles/*
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
FROM alpine:3.14
|
FROM alpine:3.16
|
||||||
RUN apk add --update bash openvpn easy-rsa && \
|
RUN apk add --update bash openvpn easy-rsa iptables && \
|
||||||
ln -s /usr/share/easy-rsa/easyrsa /usr/local/bin && \
|
ln -s /usr/share/easy-rsa/easyrsa /usr/local/bin && \
|
||||||
wget https://github.com/pashcovich/openvpn-user/releases/download/v1.0.3/openvpn-user-linux-amd64.tar.gz -O - | tar xz -C /usr/local/bin && \
|
wget https://github.com/pashcovich/openvpn-user/releases/download/v1.0.4/openvpn-user-linux-amd64.tar.gz -O - | tar xz -C /usr/local/bin && \
|
||||||
rm -rf /tmp/* /var/tmp/* /var/cache/apk/* /var/cache/distfiles/*
|
rm -rf /tmp/* /var/tmp/* /var/cache/apk/* /var/cache/distfiles/*
|
||||||
COPY setup/ /etc/openvpn/setup
|
COPY setup/ /etc/openvpn/setup
|
||||||
RUN chmod +x /etc/openvpn/setup/configure.sh
|
RUN chmod +x /etc/openvpn/setup/configure.sh
|
||||||
|
|
58
README.md
58
README.md
|
@ -65,10 +65,12 @@ cd ovpn-admin
|
||||||
|
|
||||||
(Please don't forget to configure all needed params in advance.)
|
(Please don't forget to configure all needed params in advance.)
|
||||||
|
|
||||||
### 3. Prebuilt binary (WIP)
|
### 3. Prebuilt binary
|
||||||
|
|
||||||
You can also download and use prebuilt binaries from the [releases](https://github.com/flant/ovpn-admin/releases) page — just choose a relevant tar.gz file.
|
You can also download and use prebuilt binaries from the [releases](https://github.com/flant/ovpn-admin/releases) page — just choose a relevant tar.gz file.
|
||||||
|
|
||||||
|
|
||||||
|
## Notes
|
||||||
To use password authentication (the `--auth` flag) you have to install [openvpn-user](https://github.com/pashcovich/openvpn-user/releases). This tool should be available in your `$PATH` and its binary should be executable (`+x`).
|
To use password authentication (the `--auth` flag) you have to install [openvpn-user](https://github.com/pashcovich/openvpn-user/releases). This tool should be available in your `$PATH` and its binary should be executable (`+x`).
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
@ -80,88 +82,88 @@ Flags:
|
||||||
--help show context-sensitive help (try also --help-long and --help-man)
|
--help show context-sensitive help (try also --help-long and --help-man)
|
||||||
|
|
||||||
--listen.host="0.0.0.0" host for ovpn-admin
|
--listen.host="0.0.0.0" host for ovpn-admin
|
||||||
(or $OVPN_LISTEN_HOST)
|
(or OVPN_LISTEN_HOST)
|
||||||
|
|
||||||
--listen.port="8080" port for ovpn-admin
|
--listen.port="8080" port for ovpn-admin
|
||||||
(or $OVPN_LISTEN_PROT)
|
(or OVPN_LISTEN_PROT)
|
||||||
|
|
||||||
--role="master" server role, master or slave
|
--role="master" server role, master or slave
|
||||||
(or $OVPN_ROLE)
|
(or OVPN_ROLE)
|
||||||
|
|
||||||
--master.host="http://127.0.0.1"
|
--master.host="http://127.0.0.1"
|
||||||
(or $OVPN_MASTER_HOST) URL for the master server
|
(or OVPN_MASTER_HOST) URL for the master server
|
||||||
|
|
||||||
--master.basic-auth.user="" user for master server's Basic Auth
|
--master.basic-auth.user="" user for master server's Basic Auth
|
||||||
(or $OVPN_MASTER_USER)
|
(or OVPN_MASTER_USER)
|
||||||
|
|
||||||
--master.basic-auth.password=""
|
--master.basic-auth.password=""
|
||||||
(or $OVPN_MASTER_PASSWORD) password for master server's Basic Auth
|
(or OVPN_MASTER_PASSWORD) password for master server's Basic Auth
|
||||||
|
|
||||||
--master.sync-frequency=600 master host data sync frequency in seconds
|
--master.sync-frequency=600 master host data sync frequency in seconds
|
||||||
(or $OVPN_MASTER_SYNC_FREQUENCY)
|
(or OVPN_MASTER_SYNC_FREQUENCY)
|
||||||
|
|
||||||
--master.sync-token=TOKEN master host data sync security token
|
--master.sync-token=TOKEN master host data sync security token
|
||||||
(or $OVPN_MASTER_TOKEN)
|
(or OVPN_MASTER_TOKEN)
|
||||||
|
|
||||||
--ovpn.network="172.16.100.0/24"
|
--ovpn.network="172.16.100.0/24"
|
||||||
(or $OVPN_NETWORK) NETWORK/MASK_PREFIX for OpenVPN server
|
(or OVPN_NETWORK) NETWORK/MASK_PREFIX for OpenVPN server
|
||||||
|
|
||||||
--ovpn.server=HOST:PORT:PROTOCOL ...
|
--ovpn.server=HOST:PORT:PROTOCOL ...
|
||||||
(or $OVPN_SERVER) HOST:PORT:PROTOCOL for OpenVPN server
|
(or OVPN_SERVER) HOST:PORT:PROTOCOL for OpenVPN server
|
||||||
can have multiple values
|
can have multiple values
|
||||||
|
|
||||||
--ovpn.server.behindLB enable if your OpenVPN server is behind Kubernetes
|
--ovpn.server.behindLB enable if your OpenVPN server is behind Kubernetes
|
||||||
(or $OVPN_LB) Service having the LoadBalancer type
|
(or OVPN_LB) Service having the LoadBalancer type
|
||||||
|
|
||||||
--ovpn.service="openvpn-external"
|
--ovpn.service="openvpn-external"
|
||||||
(or $OVPN_LB_SERVICE) the name of Kubernetes Service having the LoadBalancer
|
(or OVPN_LB_SERVICE) the name of Kubernetes Service having the LoadBalancer
|
||||||
type if your OpenVPN server is behind it
|
type if your OpenVPN server is behind it
|
||||||
|
|
||||||
--mgmt=main=127.0.0.1:8989 ...
|
--mgmt=main=127.0.0.1:8989 ...
|
||||||
(or $OVPN_MGMT) ALIAS=HOST:PORT for OpenVPN server mgmt interface;
|
(or OVPN_MGMT) ALIAS=HOST:PORT for OpenVPN server mgmt interface;
|
||||||
can have multiple values
|
can have multiple values
|
||||||
|
|
||||||
--metrics.path="/metrics" URL path for exposing collected metrics
|
--metrics.path="/metrics" URL path for exposing collected metrics
|
||||||
(or $OVPN_METRICS_PATH)
|
(or OVPN_METRICS_PATH)
|
||||||
|
|
||||||
--easyrsa.path="./easyrsa/" path to easyrsa dir
|
--easyrsa.path="./easyrsa/" path to easyrsa dir
|
||||||
(or $EASYRSA_PATH)
|
(or EASYRSA_PATH)
|
||||||
|
|
||||||
--easyrsa.index-path="./easyrsa/pki/index.txt"
|
--easyrsa.index-path="./easyrsa/pki/index.txt"
|
||||||
(or $OVPN_INDEX_PATH) path to easyrsa index file
|
(or OVPN_INDEX_PATH) path to easyrsa index file
|
||||||
|
|
||||||
--ccd enable client-config-dir
|
--ccd enable client-config-dir
|
||||||
(or $OVPN_CCD)
|
(or OVPN_CCD)
|
||||||
|
|
||||||
--ccd.path="./ccd" path to client-config-dir
|
--ccd.path="./ccd" path to client-config-dir
|
||||||
(or $OVPN_CCD_PATH)
|
(or OVPN_CCD_PATH)
|
||||||
|
|
||||||
--templates.clientconfig-path=""
|
--templates.clientconfig-path=""
|
||||||
(or $OVPN_TEMPLATES_CC_PATH) path to custom client.conf.tpl
|
(or OVPN_TEMPLATES_CC_PATH) path to custom client.conf.tpl
|
||||||
|
|
||||||
--templates.ccd-path="" path to custom ccd.tpl
|
--templates.ccd-path="" path to custom ccd.tpl
|
||||||
(or $OVPN_TEMPLATES_CCD_PATH)
|
(or OVPN_TEMPLATES_CCD_PATH)
|
||||||
|
|
||||||
--auth.password enable additional password authorization
|
--auth.password enable additional password authorization
|
||||||
(or $OVPN_AUTH)
|
(or OVPN_AUTH)
|
||||||
|
|
||||||
--auth.db="./easyrsa/pki/users.db"
|
--auth.db="./easyrsa/pki/users.db"
|
||||||
(or $OVPN_AUTH_DB_PATH) database path for password authorization
|
(or OVPN_AUTH_DB_PATH) database path for password authorization
|
||||||
|
|
||||||
--debug enable debug mode
|
--debug enable debug mode
|
||||||
(or $OVPN_DEBUG)
|
(or OVPN_DEBUG)
|
||||||
|
|
||||||
--verbose enable verbose mode
|
--verbose enable verbose mode
|
||||||
(or $OVPN_VERBOSE)
|
(or OVPN_VERBOSE)
|
||||||
|
|
||||||
--log.level set log level: trace, debug, info, warn, error (default info)
|
--log.level set log level: trace, debug, info, warn, error (default info)
|
||||||
(or $LOG_LEVEL)
|
(or LOG_LEVEL)
|
||||||
|
|
||||||
--log.format set log format: text, json (default text)
|
--log.format set log format: text, json (default text)
|
||||||
(or $LOG_FORMAT)
|
(or LOG_FORMAT)
|
||||||
|
|
||||||
--storage.backend storage backend: filesystem, kubernetes.secrets (default filesystem)
|
--storage.backend storage backend: filesystem, kubernetes.secrets (default filesystem)
|
||||||
(or $STORAGE_BACKEND)
|
(or STORAGE_BACKEND)
|
||||||
|
|
||||||
--version show application version
|
--version show application version
|
||||||
```
|
```
|
||||||
|
|
|
@ -10,6 +10,7 @@ services:
|
||||||
environment:
|
environment:
|
||||||
OVPN_SERVER_NET: "192.168.100.0"
|
OVPN_SERVER_NET: "192.168.100.0"
|
||||||
OVPN_SERVER_MASK: "255.255.255.0"
|
OVPN_SERVER_MASK: "255.255.255.0"
|
||||||
|
OVPN_PASSWD_AUTH: "true"
|
||||||
cap_add:
|
cap_add:
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
ports:
|
ports:
|
||||||
|
@ -24,12 +25,17 @@ services:
|
||||||
image: ovpn-admin:local
|
image: ovpn-admin:local
|
||||||
command: /app/ovpn-admin
|
command: /app/ovpn-admin
|
||||||
environment:
|
environment:
|
||||||
OVPN_DEBUG: "True"
|
OVPN_DEBUG: "true"
|
||||||
OVPN_VERBOSE: "True"
|
OVPN_VERBOSE: "true"
|
||||||
OVPN_NETWORK: "192.168.100.0/24"
|
OVPN_NETWORK: "192.168.100.0/24"
|
||||||
|
OVPN_CCD: "true"
|
||||||
|
OVPN_CCD_PATH: "/mnt/ccd"
|
||||||
EASYRSA_PATH: "/mnt/easyrsa"
|
EASYRSA_PATH: "/mnt/easyrsa"
|
||||||
OVPN_SERVER: "127.0.0.1:7777:tcp"
|
OVPN_SERVER: "127.0.0.1:7777:tcp"
|
||||||
OVPN_INDEX_PATH: "/mnt/easyrsa/pki/index.txt"
|
OVPN_INDEX_PATH: "/mnt/easyrsa/pki/index.txt"
|
||||||
|
OVPN_AUTH: "true"
|
||||||
|
OVPN_AUTH_DB_PATH: "/mnt/easyrsa/pki/users.db"
|
||||||
|
LOG_LEVEL: "debug"
|
||||||
network_mode: service:openvpn
|
network_mode: service:openvpn
|
||||||
volumes:
|
volumes:
|
||||||
- ./easyrsa_master:/mnt/easyrsa
|
- ./easyrsa_master:/mnt/easyrsa
|
||||||
|
|
10820
frontend/package-lock.json
generated
10820
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -10,14 +10,15 @@
|
||||||
"build": "cross-env NODE_ENV=production webpack --progress"
|
"build": "cross-env NODE_ENV=production webpack --progress"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.24.0",
|
"axios": "^0.27.1",
|
||||||
"bootstrap-vue": "^2.21.2",
|
"bootstrap-vue": "^2.22.0",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"vue": "^2.6.14",
|
"vue": "^2.6.14",
|
||||||
"vue-clipboard2": "^0.3.3",
|
"vue-clipboard2": "^0.3.3",
|
||||||
"vue-cookies": "^1.7.4",
|
"vue-cookies": "^1.7.4",
|
||||||
"vue-good-table": "^2.21.11",
|
"vue-good-table": "^2.21.11",
|
||||||
"vue-notification": "^1.3.20"
|
"vue-notification": "^1.3.20",
|
||||||
|
"vue-style-loader": "^4.1.3"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"> 1%",
|
"> 1%",
|
||||||
|
@ -38,7 +39,7 @@
|
||||||
"node-sass": "^7.0.1",
|
"node-sass": "^7.0.1",
|
||||||
"sass-loader": "^12.4.0",
|
"sass-loader": "^12.4.0",
|
||||||
"terser-webpack-plugin": "^5.3.0",
|
"terser-webpack-plugin": "^5.3.0",
|
||||||
"vue-loader": "^15.9.8",
|
"vue-loader": "^17.0.0",
|
||||||
"vue-template-compiler": "^2.6.14",
|
"vue-template-compiler": "^2.6.14",
|
||||||
"webpack": "^5.65.0",
|
"webpack": "^5.65.0",
|
||||||
"webpack-cli": "^4.9.1",
|
"webpack-cli": "^4.9.1",
|
||||||
|
|
|
@ -57,8 +57,8 @@ new Vue({
|
||||||
filterable: true,
|
filterable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Connection Server',
|
label: 'Active Connections',
|
||||||
field: 'ConnectionServer',
|
field: 'Connections',
|
||||||
filterable: true,
|
filterable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -107,6 +107,38 @@ new Vue({
|
||||||
showForServerRole: ['master'],
|
showForServerRole: ['master'],
|
||||||
showForModule: ["core"],
|
showForModule: ["core"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'u-delete',
|
||||||
|
label: 'Delete',
|
||||||
|
class: 'btn-danger',
|
||||||
|
showWhenStatus: 'Revoked',
|
||||||
|
showForServerRole: ['master'],
|
||||||
|
showForModule: ["core"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'u-delete',
|
||||||
|
label: 'Delete',
|
||||||
|
class: 'btn-danger',
|
||||||
|
showWhenStatus: 'Expired',
|
||||||
|
showForServerRole: ['master'],
|
||||||
|
showForModule: ["core"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'u-rotate',
|
||||||
|
label: 'Rotate',
|
||||||
|
class: 'btn-warning',
|
||||||
|
showWhenStatus: 'Revoked',
|
||||||
|
showForServerRole: ['master'],
|
||||||
|
showForModule: ["core"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'u-rotate',
|
||||||
|
label: 'Rotate',
|
||||||
|
class: 'btn-warning',
|
||||||
|
showWhenStatus: 'Expired',
|
||||||
|
showForServerRole: ['master'],
|
||||||
|
showForModule: ["core"],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'u-unrevoke',
|
name: 'u-unrevoke',
|
||||||
label: 'Unrevoke',
|
label: 'Unrevoke',
|
||||||
|
@ -161,10 +193,14 @@ new Vue({
|
||||||
newPassword: '',
|
newPassword: '',
|
||||||
passwordChangeStatus: '',
|
passwordChangeStatus: '',
|
||||||
passwordChangeMessage: '',
|
passwordChangeMessage: '',
|
||||||
|
rotateUserMessage: '',
|
||||||
|
deleteUserMessage: '',
|
||||||
modalNewUserVisible: false,
|
modalNewUserVisible: false,
|
||||||
modalShowConfigVisible: false,
|
modalShowConfigVisible: false,
|
||||||
modalShowCcdVisible: false,
|
modalShowCcdVisible: false,
|
||||||
modalChangePasswordVisible: false,
|
modalChangePasswordVisible: false,
|
||||||
|
modalRotateUserVisible: false,
|
||||||
|
modalDeleteUserVisible: false,
|
||||||
openvpnConfig: '',
|
openvpnConfig: '',
|
||||||
ccd: {
|
ccd: {
|
||||||
Name: '',
|
Name: '',
|
||||||
|
@ -204,6 +240,16 @@ new Vue({
|
||||||
_this.$notify({title: 'User ' + _this.username + ' unrevoked!', type: 'success'})
|
_this.$notify({title: 'User ' + _this.username + ' unrevoked!', type: 'success'})
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
_this.$root.$on('u-rotate', function () {
|
||||||
|
_this.u.modalRotateUserVisible = true;
|
||||||
|
var data = new URLSearchParams();
|
||||||
|
data.append('username', _this.username);
|
||||||
|
})
|
||||||
|
_this.$root.$on('u-delete', function () {
|
||||||
|
_this.u.modalDeleteUserVisible = true;
|
||||||
|
var data = new URLSearchParams();
|
||||||
|
data.append('username', _this.username);
|
||||||
|
})
|
||||||
_this.$root.$on('u-show-config', function () {
|
_this.$root.$on('u-show-config', function () {
|
||||||
_this.u.modalShowConfigVisible = true;
|
_this.u.modalShowConfigVisible = true;
|
||||||
var data = new URLSearchParams();
|
var data = new URLSearchParams();
|
||||||
|
@ -251,8 +297,8 @@ new Vue({
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
customAddressDisabled: function () {
|
customAddressDynamic: function () {
|
||||||
return this.serverRole == "master" ? this.u.ccd.ClientAddress == "dynamic" : true
|
return this.u.ccd.ClientAddress == "dynamic"
|
||||||
},
|
},
|
||||||
ccdApplyStatusCssClass: function () {
|
ccdApplyStatusCssClass: function () {
|
||||||
return this.u.ccdApplyStatus == 200 ? "alert-success" : "alert-danger"
|
return this.u.ccdApplyStatus == 200 ? "alert-success" : "alert-danger"
|
||||||
|
@ -260,6 +306,12 @@ new Vue({
|
||||||
passwordChangeStatusCssClass: function () {
|
passwordChangeStatusCssClass: function () {
|
||||||
return this.u.passwordChangeStatus == 200 ? "alert-success" : "alert-danger"
|
return this.u.passwordChangeStatus == 200 ? "alert-success" : "alert-danger"
|
||||||
},
|
},
|
||||||
|
userRotateStatusCssClass: function () {
|
||||||
|
return this.u.roatateUserStatus == 200 ? "alert-success" : "alert-danger"
|
||||||
|
},
|
||||||
|
deleteUserStatusCssClass: function () {
|
||||||
|
return this.u.deleteUserStatus == 200 ? "alert-success" : "alert-danger"
|
||||||
|
},
|
||||||
modalNewUserDisplay: function () {
|
modalNewUserDisplay: function () {
|
||||||
return this.u.modalNewUserVisible ? {display: 'flex'} : {}
|
return this.u.modalNewUserVisible ? {display: 'flex'} : {}
|
||||||
},
|
},
|
||||||
|
@ -272,6 +324,12 @@ new Vue({
|
||||||
modalChangePasswordDisplay: function () {
|
modalChangePasswordDisplay: function () {
|
||||||
return this.u.modalChangePasswordVisible ? {display: 'flex'} : {}
|
return this.u.modalChangePasswordVisible ? {display: 'flex'} : {}
|
||||||
},
|
},
|
||||||
|
modalRotateUserDisplay: function () {
|
||||||
|
return this.u.modalRotateUserVisible ? {display: 'flex'} : {}
|
||||||
|
},
|
||||||
|
modalDeleteUserDisplay: function () {
|
||||||
|
return this.u.modalDeleteUserVisible ? {display: 'flex'} : {}
|
||||||
|
},
|
||||||
revokeFilterText: function() {
|
revokeFilterText: function() {
|
||||||
return this.filters.hideRevoked ? "Show revoked" : "Hide revoked"
|
return this.filters.hideRevoked ? "Show revoked" : "Hide revoked"
|
||||||
},
|
},
|
||||||
|
@ -288,7 +346,16 @@ new Vue({
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
rowStyleClassFn: function(row) {
|
rowStyleClassFn: function(row) {
|
||||||
return row.ConnectionStatus == 'Connected' ? 'connected-user' : ''
|
if (row.ConnectionStatus == 'Connected') {
|
||||||
|
return 'connected-user'
|
||||||
|
}
|
||||||
|
if (row.AccountStatus == 'Revoked') {
|
||||||
|
return 'revoked-user'
|
||||||
|
}
|
||||||
|
if (row.AccountStatus == 'Expired') {
|
||||||
|
return 'expired-user'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
},
|
},
|
||||||
rowActionFn: function(e) {
|
rowActionFn: function(e) {
|
||||||
this.username = e.target.dataset.username;
|
this.username = e.target.dataset.username;
|
||||||
|
@ -302,14 +369,6 @@ new Vue({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
staticAddrCheckboxOnChange: function() {
|
|
||||||
var staticAddrInput = document.getElementById('static-address');
|
|
||||||
var staticAddrEnable = document.getElementById('enable-static');
|
|
||||||
|
|
||||||
staticAddrInput.disabled = !staticAddrEnable.checked;
|
|
||||||
staticAddrInput.value == "dynamic" ? staticAddrInput.value = "" : staticAddrInput.value = "dynamic";
|
|
||||||
},
|
|
||||||
|
|
||||||
getServerSetting: function() {
|
getServerSetting: function() {
|
||||||
var _this = this;
|
var _this = this;
|
||||||
axios.request(axios_cfg('api/server/settings'))
|
axios.request(axios_cfg('api/server/settings'))
|
||||||
|
@ -394,6 +453,52 @@ new Vue({
|
||||||
_this.$notify({title: 'Changing password for user ' + _this.username + ' failed!', type: 'error'})
|
_this.$notify({title: 'Changing password for user ' + _this.username + ' failed!', type: 'error'})
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
rotateUser: function(user) {
|
||||||
|
var _this = this;
|
||||||
|
|
||||||
|
_this.u.rotateUserMessage = "";
|
||||||
|
|
||||||
|
var data = new URLSearchParams();
|
||||||
|
data.append('username', user);
|
||||||
|
data.append('password', _this.u.newPassword);
|
||||||
|
|
||||||
|
axios.request(axios_cfg('api/user/rotate', data, 'form'))
|
||||||
|
.then(function(response) {
|
||||||
|
_this.u.roatateUserStatus = 200;
|
||||||
|
_this.u.newPassword = '';
|
||||||
|
_this.getUserData();
|
||||||
|
_this.u.modalRotateUserVisible = false;
|
||||||
|
_this.$notify({title: 'Certificates for user ' + _this.username + ' rotated!', type: 'success'})
|
||||||
|
})
|
||||||
|
.catch(function(error) {
|
||||||
|
_this.u.roatateUserStatus = error.response.status;
|
||||||
|
_this.u.rotateUserMessage = error.response.data.message;
|
||||||
|
_this.$notify({title: 'Rotate certificates for user ' + _this.username + ' failed!', type: 'error'})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
deleteUser: function(user) {
|
||||||
|
var _this = this;
|
||||||
|
|
||||||
|
_this.u.deleteUserMessage = "";
|
||||||
|
|
||||||
|
var data = new URLSearchParams();
|
||||||
|
data.append('username', user);
|
||||||
|
|
||||||
|
axios.request(axios_cfg('api/user/delete', data, 'form'))
|
||||||
|
.then(function(response) {
|
||||||
|
_this.u.deleteUserStatus = 200;
|
||||||
|
_this.u.newPassword = '';
|
||||||
|
_this.getUserData();
|
||||||
|
_this.u.modalDeleteUserVisible = false;
|
||||||
|
_this.$notify({title: 'User ' + _this.username + ' deleted!', type: 'success'})
|
||||||
|
})
|
||||||
|
.catch(function(error) {
|
||||||
|
_this.u.deleteUserStatus = error.response.status;
|
||||||
|
_this.u.deleteUserMessage = error.response.data.message;
|
||||||
|
_this.$notify({title: 'Deleting user ' + _this.username + ' failed!', type: 'error'})
|
||||||
|
})
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -48,6 +48,14 @@ body {
|
||||||
background-color: rgba(162, 245, 169, 0.5);
|
background-color: rgba(162, 245, 169, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.revoked-user {
|
||||||
|
background-color: rgba(198, 186, 186, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.expired-user {
|
||||||
|
background-color: rgba(255, 220, 127, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
.new-user-btn {
|
.new-user-btn {
|
||||||
margin-right: 2rem;
|
margin-right: 2rem;
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,10 +42,6 @@
|
||||||
</template>
|
</template>
|
||||||
</vue-good-table>
|
</vue-good-table>
|
||||||
|
|
||||||
<!-- <div class="d-flex justify-content-md-end">-->
|
|
||||||
<!-- <button type="button" class="btn btn-sm btn-success el-square new-user-btn" v-on:click.stop="u.ctxVisible=false;u.modalNewUserVisible=true">Add user</button>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
|
|
||||||
<div class="modal-wrapper" v-if="u.modalNewUserVisible" v-bind:style="modalNewUserDisplay">
|
<div class="modal-wrapper" v-if="u.modalNewUserVisible" v-bind:style="modalNewUserDisplay">
|
||||||
<div class="modal-dialog modal-lg">
|
<div class="modal-dialog modal-lg">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
@ -123,13 +119,11 @@
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<h5 class="static-address-label ">Static address:</h5>
|
<h5 class="static-address-label ">Static address:</h5>
|
||||||
<div class="input-group-prepend">
|
<input id="static-address" type="text" class="form-control" v-model="u.ccd.ClientAddress" placeholder="127.0.0.1">
|
||||||
<div class="input-group-text">
|
<div class="input-group-append">
|
||||||
<input id="enable-static" type="checkbox" @change="staticAddrCheckboxOnChange()" v-if="serverRole == 'master'" v-bind:checked="!customAddressDisabled">
|
<button id="static-address-clear" class="btn btn-warning" type="button" v-on:click="u.ccd.ClientAddress = 'dynamic'" v-if="serverRole == 'master'" v-bind:disabled="customAddressDynamic">Clear</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input id="static-address" type="text" class="form-control" v-model="u.ccd.ClientAddress" placeholder="127.0.0.1" v-bind:disabled="customAddressDisabled">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="d-flex ">
|
<div class="d-flex ">
|
||||||
|
@ -191,6 +185,50 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-wrapper" v-if="u.modalRotateUserVisible" v-bind:style="modalRotateUserDisplay">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4>Confirm rotating certificates for user: <strong>{{ username }}</strong></h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" v-if="modulesEnabled.includes('passwdAuth')">
|
||||||
|
<h4>Enter new password:</h4>
|
||||||
|
<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.rotateUserMessage.length > 0">
|
||||||
|
<div class="alert" v-bind:class="userRotateStatusCssClass" role="alert" >
|
||||||
|
{{ u.rotateUserMessage }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-danger el-square modal-el-margin" v-on:click.stop="rotateUser(username)">Rotate</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.rotateUserMessage='';u.modalRotateUserVisible=false">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-wrapper" v-if="u.modalDeleteUserVisible" v-bind:style="modalDeleteUserDisplay">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4>Confirm deleting user: <strong>{{ username }}</strong></h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer justify-content-center" v-if="u.deleteUserMessage.length > 0">
|
||||||
|
<div class="alert" v-bind:class="deleteUserStatusCssClass" role="alert" >
|
||||||
|
{{ u.deleteUserMessage }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-danger el-square modal-el-margin" v-on:click.stop="deleteUser(username)">Delete</button>
|
||||||
|
<button type="button" class="btn btn-primary el-square d-flex justify-content-sm-end modal-el-margin" v-on:click.stop="u.deleteUserMessage='';u.modalDeleteUserVisible=false">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<notifications position="bottom left" :speed="900" />
|
<notifications position="bottom left" :speed="900" />
|
||||||
</div>
|
</div>
|
||||||
<script src="dist/bundle.min.js"></script>
|
<script src="dist/bundle.min.js"></script>
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
// Generated using webpack-cli https://github.com/webpack/webpack-cli
|
|
||||||
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
//const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||||
|
|
||||||
const isProduction = process.env.NODE_ENV == 'production';
|
module.exports = {
|
||||||
|
mode: 'production',
|
||||||
|
|
||||||
const config = {
|
|
||||||
entry: {
|
entry: {
|
||||||
bundle: [
|
bundle: [
|
||||||
'./src/main.js',
|
'./src/main.js',
|
||||||
|
@ -20,6 +17,7 @@ const config = {
|
||||||
filename: '[name].min.js'
|
filename: '[name].min.js'
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
//new BundleAnalyzerPlugin(),
|
||||||
],
|
],
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
|
@ -32,6 +30,7 @@ const config = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.js$/,
|
test: /\.js$/,
|
||||||
|
//exclude: /node_modules\/(?!bootstrap-vue\/src\/)/,
|
||||||
exclude: /node_modules/,
|
exclude: /node_modules/,
|
||||||
loader: 'babel-loader',
|
loader: 'babel-loader',
|
||||||
options: {
|
options: {
|
||||||
|
@ -42,14 +41,10 @@ const config = {
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'vue$': 'vue/dist/vue.esm.js'
|
'vue$': 'vue/dist/vue.esm.js',
|
||||||
|
//'bootstrap-vue$': 'bootstrap-vue/src/index.js'
|
||||||
},
|
},
|
||||||
extensions: ['*', '.js', '.vue', '.json']
|
extensions: ['*', '.js', '.vue', '.json']
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|
||||||
module.exports = () => {
|
|
||||||
config.mode = 'production';
|
|
||||||
|
|
||||||
return config;
|
|
||||||
};
|
|
||||||
|
|
4
go.mod
4
go.mod
|
@ -8,6 +8,9 @@ require (
|
||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
||||||
k8s.io/apimachinery v0.23.1
|
k8s.io/apimachinery v0.23.1
|
||||||
k8s.io/client-go v0.23.1
|
k8s.io/client-go v0.23.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
|
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
|
||||||
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
|
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
@ -22,6 +25,7 @@ require (
|
||||||
github.com/golang/protobuf v1.5.2 // indirect
|
github.com/golang/protobuf v1.5.2 // indirect
|
||||||
github.com/google/go-cmp v0.5.6 // indirect
|
github.com/google/go-cmp v0.5.6 // indirect
|
||||||
github.com/google/gofuzz v1.2.0 // indirect
|
github.com/google/gofuzz v1.2.0 // indirect
|
||||||
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
github.com/googleapis/gnostic v0.5.5 // indirect
|
github.com/googleapis/gnostic v0.5.5 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/karrick/godirwalk v1.16.1 // indirect
|
github.com/karrick/godirwalk v1.16.1 // indirect
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -263,6 +263,8 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4
|
||||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||||
github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=
|
github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=
|
||||||
|
|
|
@ -32,7 +32,7 @@ func runBash(script string) string {
|
||||||
cmd := exec.Command("bash", "-c", script)
|
cmd := exec.Command("bash", "-c", script)
|
||||||
stdout, err := cmd.CombinedOutput()
|
stdout, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return (fmt.Sprint(err) + " : " + string(stdout))
|
return fmt.Sprint(err) + " : " + string(stdout)
|
||||||
}
|
}
|
||||||
return string(stdout)
|
return string(stdout)
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ func fExist(path string) bool {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return false
|
return false
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatalf("fExist: %s", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,7 +53,8 @@ func fExist(path string) bool {
|
||||||
func fRead(path string) string {
|
func fRead(path string) string {
|
||||||
content, err := ioutil.ReadFile(path)
|
content, err := ioutil.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Warning(err)
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(content)
|
return string(content)
|
||||||
|
|
|
@ -22,7 +22,7 @@ import (
|
||||||
const (
|
const (
|
||||||
secretCA = "openvpn-pki-ca"
|
secretCA = "openvpn-pki-ca"
|
||||||
secretServer = "openvpn-pki-server"
|
secretServer = "openvpn-pki-server"
|
||||||
secretClientTmpl = "openvpn-pki-%s"
|
secretClientTmpl = "openvpn-pki-%d"
|
||||||
secretCRL = "openvpn-pki-crl"
|
secretCRL = "openvpn-pki-crl"
|
||||||
secretIndexTxt = "openvpn-pki-index-txt"
|
secretIndexTxt = "openvpn-pki-index-txt"
|
||||||
secretDHandTA = "openvpn-pki-dh-and-ta"
|
secretDHandTA = "openvpn-pki-dh-and-ta"
|
||||||
|
@ -228,11 +228,11 @@ func (openVPNPKI *OpenVPNPKI) indexTxtUpdate() (err error) {
|
||||||
log.Trace(cert.Subject.CommonName)
|
log.Trace(cert.Subject.CommonName)
|
||||||
|
|
||||||
if secret.Annotations["revokedAt"] == "" {
|
if secret.Annotations["revokedAt"] == "" {
|
||||||
indexTxt += fmt.Sprintf("%s\t%s\t\t%s\t%s\t%s\n", "V", cert.NotAfter.Format(indexTxtDateFormat), cert.SerialNumber.String(), "unknown", "/CN="+cert.Subject.CommonName)
|
indexTxt += fmt.Sprintf("%s\t%s\t\t%s\t%s\t%s\n", "V", cert.NotAfter.Format(indexTxtDateFormat), fmt.Sprintf("%d", cert.SerialNumber), "unknown", "/CN="+secret.Annotations["name"])
|
||||||
} else if cert.NotAfter.Before(time.Now()) {
|
} else if cert.NotAfter.Before(time.Now()) {
|
||||||
indexTxt += fmt.Sprintf("%s\t%s\t\t%s\t%s\t%s\n", "E", cert.NotAfter.Format(indexTxtDateFormat), cert.SerialNumber.String(), "unknown", "/CN="+cert.Subject.CommonName)
|
indexTxt += fmt.Sprintf("%s\t%s\t\t%s\t%s\t%s\n", "E", cert.NotAfter.Format(indexTxtDateFormat), fmt.Sprintf("%d", cert.SerialNumber), "unknown", "/CN="+secret.Annotations["name"])
|
||||||
} else {
|
} else {
|
||||||
indexTxt += fmt.Sprintf("%s\t%s\t%s\t%s\t%s\t%s\n", "R", cert.NotAfter.Format(indexTxtDateFormat), secret.Annotations["revokedAt"], cert.SerialNumber.String(), "unknown", "/CN="+cert.Subject.CommonName)
|
indexTxt += fmt.Sprintf("%s\t%s\t%s\t%s\t%s\t%s\n", "R", cert.NotAfter.Format(indexTxtDateFormat), secret.Annotations["revokedAt"], fmt.Sprintf("%d", cert.SerialNumber), "unknown", "/CN="+secret.Annotations["name"])
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -336,7 +336,7 @@ func (openVPNPKI *OpenVPNPKI) easyrsaBuildClient(commonName string) (err error)
|
||||||
"notBefore": clientCert.NotBefore.Format(indexTxtDateFormat),
|
"notBefore": clientCert.NotBefore.Format(indexTxtDateFormat),
|
||||||
"notAfter": clientCert.NotAfter.Format(indexTxtDateFormat),
|
"notAfter": clientCert.NotAfter.Format(indexTxtDateFormat),
|
||||||
"revokedAt": "",
|
"revokedAt": "",
|
||||||
"serialNumber": clientCert.SerialNumber.String(),
|
"serialNumber": fmt.Sprintf("%d", clientCert.SerialNumber),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -447,6 +447,78 @@ func (openVPNPKI *OpenVPNPKI) easyrsaUnrevoke(commonName string) (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (openVPNPKI *OpenVPNPKI) easyrsaRotate(commonName, newPassword string) (err error) {
|
||||||
|
secret, err := openVPNPKI.secretGetByLabels("name=" + commonName)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
secret.Annotations["commonName"] = "REVOKED" + commonName
|
||||||
|
secret.Labels["name"] = "REVOKED" + commonName
|
||||||
|
secret.Labels["revokedForever"] = "true"
|
||||||
|
|
||||||
|
_, err = openVPNPKI.KubeClient.CoreV1().Secrets(namespace).Update(context.TODO(), secret, metav1.UpdateOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = openVPNPKI.easyrsaBuildClient(commonName)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = openVPNPKI.indexTxtUpdate()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = openVPNPKI.updateIndexTxtOnDisk()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = openVPNPKI.easyrsaGenCRL()
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = openVPNPKI.updateCRLOnDisk()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
func (openVPNPKI *OpenVPNPKI) easyrsaDelete(commonName string) (err error) {
|
||||||
|
secret, err := openVPNPKI.secretGetByLabels("name=" + commonName)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
secret.Annotations["commonName"] = "DELETED" + commonName
|
||||||
|
secret.Labels["name"] = "DELETED" + commonName
|
||||||
|
secret.Labels["revokedForever"] = "true"
|
||||||
|
|
||||||
|
_, err = openVPNPKI.KubeClient.CoreV1().Secrets(namespace).Update(context.TODO(), secret, metav1.UpdateOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = openVPNPKI.indexTxtUpdate()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = openVPNPKI.updateIndexTxtOnDisk()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = openVPNPKI.easyrsaGenCRL()
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = openVPNPKI.updateCRLOnDisk()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (openVPNPKI *OpenVPNPKI) secretGetClientCert(name string) (cert ClientCert, err error) {
|
func (openVPNPKI *OpenVPNPKI) secretGetClientCert(name string) (cert ClientCert, err error) {
|
||||||
secret, err := openVPNPKI.secretGetByName(name)
|
secret, err := openVPNPKI.secretGetByName(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
225
main.go
225
main.go
|
@ -8,7 +8,12 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/google/uuid"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
"k8s.io/client-go/rest"
|
||||||
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
@ -18,10 +23,6 @@ import (
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
"k8s.io/client-go/kubernetes"
|
|
||||||
"k8s.io/client-go/rest"
|
|
||||||
|
|
||||||
"github.com/gobuffalo/packr/v2"
|
"github.com/gobuffalo/packr/v2"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
@ -73,7 +74,7 @@ var (
|
||||||
certsArchivePath = "/tmp/" + certsArchiveFileName
|
certsArchivePath = "/tmp/" + certsArchiveFileName
|
||||||
ccdArchivePath = "/tmp/" + ccdArchiveFileName
|
ccdArchivePath = "/tmp/" + ccdArchiveFileName
|
||||||
|
|
||||||
version = "1.7.5"
|
version = "2.0.0"
|
||||||
)
|
)
|
||||||
|
|
||||||
var logLevels = map[string]log.Level{
|
var logLevels = map[string]log.Level{
|
||||||
|
@ -122,7 +123,13 @@ var (
|
||||||
|
|
||||||
ovpnClientsConnected = prometheus.NewGauge(prometheus.GaugeOpts{
|
ovpnClientsConnected = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||||
Name: "ovpn_clients_connected",
|
Name: "ovpn_clients_connected",
|
||||||
Help: "connected openvpn users",
|
Help: "total connected openvpn clients",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
ovpnUniqClientsConnected = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||||
|
Name: "ovpn_uniq_clients_connected",
|
||||||
|
Help: "uniq connected openvpn clients",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -198,7 +205,7 @@ type OpenvpnClient struct {
|
||||||
ExpirationDate string `json:"ExpirationDate"`
|
ExpirationDate string `json:"ExpirationDate"`
|
||||||
RevocationDate string `json:"RevocationDate"`
|
RevocationDate string `json:"RevocationDate"`
|
||||||
ConnectionStatus string `json:"ConnectionStatus"`
|
ConnectionStatus string `json:"ConnectionStatus"`
|
||||||
ConnectionServer string `json:"ConnectionServer"`
|
Connections int `json:"Connections"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ccdRoute struct {
|
type ccdRoute struct {
|
||||||
|
@ -259,6 +266,7 @@ func (oAdmin *OvpnAdmin) userCreateHandler(w http.ResponseWriter, r *http.Reques
|
||||||
userCreated, userCreateStatus := oAdmin.userCreate(r.FormValue("username"), r.FormValue("password"))
|
userCreated, userCreateStatus := oAdmin.userCreate(r.FormValue("username"), r.FormValue("password"))
|
||||||
|
|
||||||
if userCreated {
|
if userCreated {
|
||||||
|
oAdmin.clients = oAdmin.usersList()
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
fmt.Fprintf(w, userCreateStatus)
|
fmt.Fprintf(w, userCreateStatus)
|
||||||
return
|
return
|
||||||
|
@ -266,6 +274,25 @@ func (oAdmin *OvpnAdmin) userCreateHandler(w http.ResponseWriter, r *http.Reques
|
||||||
http.Error(w, userCreateStatus, http.StatusUnprocessableEntity)
|
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()
|
||||||
|
fmt.Fprintf(w, "%s", oAdmin.userRotate(r.FormValue("username"), r.FormValue("password")))
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
fmt.Fprintf(w, "%s", oAdmin.userDelete(r.FormValue("username")))
|
||||||
|
}
|
||||||
|
|
||||||
func (oAdmin *OvpnAdmin) userRevokeHandler(w http.ResponseWriter, r *http.Request) {
|
func (oAdmin *OvpnAdmin) userRevokeHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Info(r.RemoteAddr, r.RequestURI)
|
log.Info(r.RemoteAddr, r.RequestURI)
|
||||||
|
@ -465,7 +492,11 @@ func main() {
|
||||||
ovpnAdmin.modules = append(ovpnAdmin.modules, "core")
|
ovpnAdmin.modules = append(ovpnAdmin.modules, "core")
|
||||||
|
|
||||||
if *authByPassword {
|
if *authByPassword {
|
||||||
|
if *storageBackend != "kubernetes.secrets" {
|
||||||
ovpnAdmin.modules = append(ovpnAdmin.modules, "passwdAuth")
|
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 {
|
if *ccdEnabled {
|
||||||
|
@ -487,6 +518,8 @@ func main() {
|
||||||
http.HandleFunc("/api/users/list", ovpnAdmin.userListHandler)
|
http.HandleFunc("/api/users/list", ovpnAdmin.userListHandler)
|
||||||
http.HandleFunc("/api/user/create", ovpnAdmin.userCreateHandler)
|
http.HandleFunc("/api/user/create", ovpnAdmin.userCreateHandler)
|
||||||
http.HandleFunc("/api/user/change-password", ovpnAdmin.userChangePasswordHandler)
|
http.HandleFunc("/api/user/change-password", ovpnAdmin.userChangePasswordHandler)
|
||||||
|
http.HandleFunc("/api/user/rotate", ovpnAdmin.userRotateHandler)
|
||||||
|
http.HandleFunc("/api/user/delete", ovpnAdmin.userDeleteHandler)
|
||||||
http.HandleFunc("/api/user/revoke", ovpnAdmin.userRevokeHandler)
|
http.HandleFunc("/api/user/revoke", ovpnAdmin.userRevokeHandler)
|
||||||
http.HandleFunc("/api/user/unrevoke", ovpnAdmin.userUnrevokeHandler)
|
http.HandleFunc("/api/user/unrevoke", ovpnAdmin.userUnrevokeHandler)
|
||||||
http.HandleFunc("/api/user/config/show", ovpnAdmin.userShowConfigHandler)
|
http.HandleFunc("/api/user/config/show", ovpnAdmin.userShowConfigHandler)
|
||||||
|
@ -522,6 +555,7 @@ func (oAdmin *OvpnAdmin) registerMetrics() {
|
||||||
oAdmin.promRegistry.MustRegister(ovpnClientsTotal)
|
oAdmin.promRegistry.MustRegister(ovpnClientsTotal)
|
||||||
oAdmin.promRegistry.MustRegister(ovpnClientsRevoked)
|
oAdmin.promRegistry.MustRegister(ovpnClientsRevoked)
|
||||||
oAdmin.promRegistry.MustRegister(ovpnClientsConnected)
|
oAdmin.promRegistry.MustRegister(ovpnClientsConnected)
|
||||||
|
oAdmin.promRegistry.MustRegister(ovpnUniqClientsConnected)
|
||||||
oAdmin.promRegistry.MustRegister(ovpnClientsExpired)
|
oAdmin.promRegistry.MustRegister(ovpnClientsExpired)
|
||||||
oAdmin.promRegistry.MustRegister(ovpnClientCertificateExpire)
|
oAdmin.promRegistry.MustRegister(ovpnClientCertificateExpire)
|
||||||
oAdmin.promRegistry.MustRegister(ovpnClientConnectionInfo)
|
oAdmin.promRegistry.MustRegister(ovpnClientConnectionInfo)
|
||||||
|
@ -544,6 +578,7 @@ func (oAdmin *OvpnAdmin) updateState() {
|
||||||
ovpnClientBytesReceived.Reset()
|
ovpnClientBytesReceived.Reset()
|
||||||
ovpnClientConnectionFrom.Reset()
|
ovpnClientConnectionFrom.Reset()
|
||||||
ovpnClientConnectionInfo.Reset()
|
ovpnClientConnectionInfo.Reset()
|
||||||
|
ovpnClientCertificateExpire.Reset()
|
||||||
go oAdmin.setState()
|
go oAdmin.setState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -670,8 +705,10 @@ func (oAdmin *OvpnAdmin) parseCcd(username string) Ccd {
|
||||||
if *storageBackend == "kubernetes.secrets" {
|
if *storageBackend == "kubernetes.secrets" {
|
||||||
txtLinesArray = strings.Split(app.secretGetCcd(ccd.User), "\n")
|
txtLinesArray = strings.Split(app.secretGetCcd(ccd.User), "\n")
|
||||||
} else {
|
} else {
|
||||||
|
if fExist(*ccdDir + "/" + username) {
|
||||||
txtLinesArray = strings.Split(fRead(*ccdDir+"/"+username), "\n")
|
txtLinesArray = strings.Split(fRead(*ccdDir+"/"+username), "\n")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, v := range txtLinesArray {
|
for _, v := range txtLinesArray {
|
||||||
str := strings.Fields(v)
|
str := strings.Fields(v)
|
||||||
|
@ -808,36 +845,42 @@ func (oAdmin *OvpnAdmin) usersList() []OpenvpnClient {
|
||||||
validCerts := 0
|
validCerts := 0
|
||||||
revokedCerts := 0
|
revokedCerts := 0
|
||||||
expiredCerts := 0
|
expiredCerts := 0
|
||||||
connectedUsers := 0
|
connectedUniqUsers := 0
|
||||||
|
totalActiveConnections := 0
|
||||||
apochNow := time.Now().Unix()
|
apochNow := time.Now().Unix()
|
||||||
|
|
||||||
for _, line := range indexTxtParser(fRead(*indexTxtPath)) {
|
for _, line := range indexTxtParser(fRead(*indexTxtPath)) {
|
||||||
if line.Identity != "server" {
|
if line.Identity != "server" && !strings.Contains(line.Identity, "REVOKED") && !strings.Contains(line.Identity, "DELETED") {
|
||||||
totalCerts += 1
|
totalCerts += 1
|
||||||
ovpnClient := OpenvpnClient{Identity: line.Identity, ExpirationDate: parseDateToString(indexTxtDateLayout, line.ExpirationDate, stringDateFormat)}
|
ovpnClient := OpenvpnClient{Identity: line.Identity, ExpirationDate: parseDateToString(indexTxtDateLayout, line.ExpirationDate, stringDateFormat)}
|
||||||
switch {
|
switch {
|
||||||
case line.Flag == "V":
|
case line.Flag == "V":
|
||||||
ovpnClient.AccountStatus = "Active"
|
ovpnClient.AccountStatus = "Active"
|
||||||
ovpnClientCertificateExpire.WithLabelValues(line.Identity).Set(float64((parseDateToUnix(indexTxtDateLayout, line.ExpirationDate) - apochNow) / 3600 / 24))
|
|
||||||
validCerts += 1
|
validCerts += 1
|
||||||
case line.Flag == "R":
|
case line.Flag == "R":
|
||||||
ovpnClient.AccountStatus = "Revoked"
|
ovpnClient.AccountStatus = "Revoked"
|
||||||
ovpnClient.RevocationDate = parseDateToString(indexTxtDateLayout, line.RevocationDate, stringDateFormat)
|
ovpnClient.RevocationDate = parseDateToString(indexTxtDateLayout, line.RevocationDate, stringDateFormat)
|
||||||
ovpnClientCertificateExpire.WithLabelValues(line.Identity).Set(float64((parseDateToUnix(indexTxtDateLayout, line.ExpirationDate) - apochNow) / 3600 / 24))
|
|
||||||
revokedCerts += 1
|
revokedCerts += 1
|
||||||
case line.Flag == "E":
|
case line.Flag == "E":
|
||||||
ovpnClient.AccountStatus = "Expired"
|
ovpnClient.AccountStatus = "Expired"
|
||||||
ovpnClientCertificateExpire.WithLabelValues(line.Identity).Set(float64((parseDateToUnix(indexTxtDateLayout, line.ExpirationDate) - apochNow) / 3600 / 24))
|
|
||||||
expiredCerts += 1
|
expiredCerts += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
ovpnClient.ConnectionServer = ""
|
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)
|
userConnected, userConnectedTo := isUserConnected(line.Identity, oAdmin.activeClients)
|
||||||
if userConnected {
|
if userConnected {
|
||||||
ovpnClient.ConnectionStatus = "Connected"
|
ovpnClient.ConnectionStatus = "Connected"
|
||||||
ovpnClient.ConnectionServer = userConnectedTo
|
for _ = range userConnectedTo {
|
||||||
connectedUsers += 1
|
ovpnClient.Connections += 1
|
||||||
|
totalActiveConnections += 1
|
||||||
|
}
|
||||||
|
connectedUniqUsers += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
users = append(users, ovpnClient)
|
users = append(users, ovpnClient)
|
||||||
|
@ -856,7 +899,8 @@ func (oAdmin *OvpnAdmin) usersList() []OpenvpnClient {
|
||||||
ovpnClientsTotal.Set(float64(totalCerts))
|
ovpnClientsTotal.Set(float64(totalCerts))
|
||||||
ovpnClientsRevoked.Set(float64(revokedCerts))
|
ovpnClientsRevoked.Set(float64(revokedCerts))
|
||||||
ovpnClientsExpired.Set(float64(expiredCerts))
|
ovpnClientsExpired.Set(float64(expiredCerts))
|
||||||
ovpnClientsConnected.Set(float64(connectedUsers))
|
ovpnClientsConnected.Set(float64(totalActiveConnections))
|
||||||
|
ovpnUniqClientsConnected.Set(float64(connectedUniqUsers))
|
||||||
|
|
||||||
return users
|
return users
|
||||||
}
|
}
|
||||||
|
@ -890,7 +934,7 @@ func (oAdmin *OvpnAdmin) userCreate(username, password string) (bool, string) {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
o := runBash(fmt.Sprintf("date +%%Y-%%m-%%d\\ %%H:%%M:%%S && cd %s && easyrsa build-client-full %s nopass", *easyrsaDirPath, username))
|
o := runBash(fmt.Sprintf("cd %s && easyrsa build-client-full %s nopass", *easyrsaDirPath, username))
|
||||||
log.Debug(o)
|
log.Debug(o)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -901,7 +945,7 @@ func (oAdmin *OvpnAdmin) userCreate(username, password string) (bool, string) {
|
||||||
|
|
||||||
log.Infof("Certificate for user %s issued", username)
|
log.Infof("Certificate for user %s issued", username)
|
||||||
|
|
||||||
oAdmin.clients = oAdmin.usersList()
|
//oAdmin.clients = oAdmin.usersList()
|
||||||
|
|
||||||
return true, ucErr
|
return true, ucErr
|
||||||
}
|
}
|
||||||
|
@ -935,13 +979,14 @@ func (oAdmin *OvpnAdmin) userChangePassword(username, password string) (bool, st
|
||||||
return false, "User does not exist"
|
return false, "User does not exist"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (oAdmin *OvpnAdmin) getUserStatistic(username string) clientStatus {
|
func (oAdmin *OvpnAdmin) getUserStatistic(username string) []clientStatus {
|
||||||
|
var userStatistic []clientStatus
|
||||||
for _, u := range oAdmin.activeClients {
|
for _, u := range oAdmin.activeClients {
|
||||||
if u.CommonName == username {
|
if u.CommonName == username {
|
||||||
return u
|
userStatistic = append(userStatistic, u)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return clientStatus{}
|
return userStatistic
|
||||||
}
|
}
|
||||||
|
|
||||||
func (oAdmin *OvpnAdmin) userRevoke(username string) string {
|
func (oAdmin *OvpnAdmin) userRevoke(username string) string {
|
||||||
|
@ -968,10 +1013,13 @@ func (oAdmin *OvpnAdmin) userRevoke(username string) string {
|
||||||
userConnected, userConnectedTo := isUserConnected(username, oAdmin.activeClients)
|
userConnected, userConnectedTo := isUserConnected(username, oAdmin.activeClients)
|
||||||
log.Tracef("User %s connected: %t", username, userConnected)
|
log.Tracef("User %s connected: %t", username, userConnected)
|
||||||
if userConnected {
|
if userConnected {
|
||||||
oAdmin.mgmtKillUserConnection(username, userConnectedTo)
|
for _, connection := range userConnectedTo {
|
||||||
log.Infof("Session for user \"%s\" session killed", username)
|
oAdmin.mgmtKillUserConnection(username, connection)
|
||||||
|
log.Infof("Session for user \"%s\" killed", username)
|
||||||
}
|
}
|
||||||
oAdmin.clients = oAdmin.usersList()
|
}
|
||||||
|
|
||||||
|
oAdmin.setState()
|
||||||
return fmt.Sprintln(shellOut)
|
return fmt.Sprintln(shellOut)
|
||||||
}
|
}
|
||||||
log.Infof("user \"%s\" not found", username)
|
log.Infof("user \"%s\" not found", username)
|
||||||
|
@ -989,35 +1037,33 @@ func (oAdmin *OvpnAdmin) userUnrevoke(username string) string {
|
||||||
// check certificate revoked flag 'R'
|
// check certificate revoked flag 'R'
|
||||||
usersFromIndexTxt := indexTxtParser(fRead(*indexTxtPath))
|
usersFromIndexTxt := indexTxtParser(fRead(*indexTxtPath))
|
||||||
for i := range usersFromIndexTxt {
|
for i := range usersFromIndexTxt {
|
||||||
if usersFromIndexTxt[i].DistinguishedName == ("/CN=" + username) {
|
if usersFromIndexTxt[i].DistinguishedName == "/CN="+username {
|
||||||
if usersFromIndexTxt[i].Flag == "R" {
|
if usersFromIndexTxt[i].Flag == "R" {
|
||||||
|
|
||||||
usersFromIndexTxt[i].Flag = "V"
|
usersFromIndexTxt[i].Flag = "V"
|
||||||
usersFromIndexTxt[i].RevocationDate = ""
|
usersFromIndexTxt[i].RevocationDate = ""
|
||||||
o := runBash(fmt.Sprintf("cd %s && cp pki/revoked/certs_by_serial/%s.crt pki/issued/%s.crt", *easyrsaDirPath, usersFromIndexTxt[i].SerialNumber, username))
|
|
||||||
//fmt.Println(o)
|
_ = runBash(fmt.Sprintf("cd %s && cp pki/revoked/certs_by_serial/%s.crt pki/issued/%s.crt", *easyrsaDirPath, usersFromIndexTxt[i].SerialNumber, username))
|
||||||
o = runBash(fmt.Sprintf("cd %s && cp pki/revoked/certs_by_serial/%s.crt pki/certs_by_serial/%s.pem", *easyrsaDirPath, usersFromIndexTxt[i].SerialNumber, usersFromIndexTxt[i].SerialNumber))
|
_ = runBash(fmt.Sprintf("cd %s && cp pki/revoked/certs_by_serial/%s.crt pki/certs_by_serial/%s.pem", *easyrsaDirPath, usersFromIndexTxt[i].SerialNumber, usersFromIndexTxt[i].SerialNumber))
|
||||||
//fmt.Println(o)
|
_ = runBash(fmt.Sprintf("cd %s && cp pki/revoked/private_by_serial/%s.key pki/private/%s.key", *easyrsaDirPath, usersFromIndexTxt[i].SerialNumber, username))
|
||||||
o = runBash(fmt.Sprintf("cd %s && cp pki/revoked/private_by_serial/%s.key pki/private/%s.key", *easyrsaDirPath, usersFromIndexTxt[i].SerialNumber, username))
|
_ = runBash(fmt.Sprintf("cd %s && cp pki/revoked/reqs_by_serial/%s.req pki/reqs/%s.req", *easyrsaDirPath, usersFromIndexTxt[i].SerialNumber, username))
|
||||||
//fmt.Println(o)
|
|
||||||
o = runBash(fmt.Sprintf("cd %s && cp pki/revoked/reqs_by_serial/%s.req pki/reqs/%s.req", *easyrsaDirPath, usersFromIndexTxt[i].SerialNumber, username))
|
|
||||||
//fmt.Println(o)
|
|
||||||
fWrite(*indexTxtPath, renderIndexTxt(usersFromIndexTxt))
|
fWrite(*indexTxtPath, renderIndexTxt(usersFromIndexTxt))
|
||||||
//fmt.Print(renderIndexTxt(usersFromIndexTxt))
|
|
||||||
o = runBash(fmt.Sprintf("cd %s && easyrsa gen-crl", *easyrsaDirPath))
|
_ = runBash(fmt.Sprintf("cd %s && easyrsa gen-crl", *easyrsaDirPath))
|
||||||
//fmt.Println(o)
|
|
||||||
if *authByPassword {
|
if *authByPassword {
|
||||||
o = runBash(fmt.Sprintf("openvpn-user restore --db-path %s --user %s", *authDatabase, username))
|
_ = runBash(fmt.Sprintf("openvpn-user restore --db-path %s --user %s", *authDatabase, username))
|
||||||
//fmt.Println(o)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
crlFix()
|
crlFix()
|
||||||
o = ""
|
|
||||||
log.Trace(o)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fWrite(*indexTxtPath, renderIndexTxt(usersFromIndexTxt))
|
fWrite(*indexTxtPath, renderIndexTxt(usersFromIndexTxt))
|
||||||
fmt.Print(renderIndexTxt(usersFromIndexTxt))
|
//fmt.Print(renderIndexTxt(usersFromIndexTxt))
|
||||||
}
|
}
|
||||||
crlFix()
|
crlFix()
|
||||||
oAdmin.clients = oAdmin.usersList()
|
oAdmin.clients = oAdmin.usersList()
|
||||||
|
@ -1026,11 +1072,91 @@ func (oAdmin *OvpnAdmin) userUnrevoke(username string) string {
|
||||||
return fmt.Sprintf("{\"msg\":\"User \"%s\" not found\"}", username)
|
return fmt.Sprintf("{\"msg\":\"User \"%s\" not found\"}", username)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (oAdmin *OvpnAdmin) userRotate(username, newPassword string) string {
|
||||||
|
if checkUserExist(username) {
|
||||||
|
if *storageBackend == "kubernetes.secrets" {
|
||||||
|
err := app.easyrsaRotate(username, newPassword)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
uniqHash := strings.Replace(uuid.New().String(), "-", "", -1)
|
||||||
|
var oldUserIndex, newUserIndex int
|
||||||
|
usersFromIndexTxt := indexTxtParser(fRead(*indexTxtPath))
|
||||||
|
for i := range usersFromIndexTxt {
|
||||||
|
if usersFromIndexTxt[i].DistinguishedName == "/CN="+username {
|
||||||
|
usersFromIndexTxt[i].DistinguishedName = "/CN=REVOKED" + username + "-" + uniqHash
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fWrite(*indexTxtPath, renderIndexTxt(usersFromIndexTxt))
|
||||||
|
oAdmin.userCreate(username, newPassword)
|
||||||
|
usersFromIndexTxt = indexTxtParser(fRead(*indexTxtPath))
|
||||||
|
for i := range usersFromIndexTxt {
|
||||||
|
if usersFromIndexTxt[i].DistinguishedName == "/CN="+username {
|
||||||
|
newUserIndex = i
|
||||||
|
}
|
||||||
|
if usersFromIndexTxt[i].DistinguishedName == "/CN=REVOKED"+username+"-"+uniqHash {
|
||||||
|
oldUserIndex = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
usersFromIndexTxt[oldUserIndex], usersFromIndexTxt[newUserIndex] = usersFromIndexTxt[newUserIndex], usersFromIndexTxt[oldUserIndex]
|
||||||
|
|
||||||
|
fWrite(*indexTxtPath, renderIndexTxt(usersFromIndexTxt))
|
||||||
|
_ = runBash(fmt.Sprintf("cd %s && easyrsa gen-crl", *easyrsaDirPath))
|
||||||
|
}
|
||||||
|
crlFix()
|
||||||
|
oAdmin.clients = oAdmin.usersList()
|
||||||
|
return fmt.Sprintf("{\"msg\":\"User %s successfully rotated\"}", username)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("{\"msg\":\"User \"%s\" not found\"}", username)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (oAdmin *OvpnAdmin) userDelete(username string) 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=DELETED" + username + "-" + uniqHash
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fWrite(*indexTxtPath, renderIndexTxt(usersFromIndexTxt))
|
||||||
|
_ = runBash(fmt.Sprintf("cd %s && easyrsa gen-crl", *easyrsaDirPath))
|
||||||
|
}
|
||||||
|
crlFix()
|
||||||
|
oAdmin.clients = oAdmin.usersList()
|
||||||
|
return fmt.Sprintf("{\"msg\":\"User %s successfully deleted\"}", username)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("{\"msg\":\"User \"%s\" not found\"}", username)
|
||||||
|
}
|
||||||
|
|
||||||
func (oAdmin *OvpnAdmin) mgmtRead(conn net.Conn) string {
|
func (oAdmin *OvpnAdmin) mgmtRead(conn net.Conn) string {
|
||||||
buf := make([]byte, 32768)
|
recvData := make([]byte, 32768)
|
||||||
bufLen, _ := conn.Read(buf)
|
var out string
|
||||||
s := string(buf[:bufLen])
|
var n int
|
||||||
return s
|
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 {
|
func (oAdmin *OvpnAdmin) mgmtConnectedUsersParser(text, serverName string) []clientStatus {
|
||||||
|
@ -1188,13 +1314,16 @@ func (oAdmin *OvpnAdmin) mgmtSetTimeFormat() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func isUserConnected(username string, connectedUsers []clientStatus) (bool, string) {
|
func isUserConnected(username string, connectedUsers []clientStatus) (bool, []string) {
|
||||||
|
var connections []string
|
||||||
|
var connected = false
|
||||||
for _, connectedUser := range connectedUsers {
|
for _, connectedUser := range connectedUsers {
|
||||||
if connectedUser.CommonName == username {
|
if connectedUser.CommonName == username {
|
||||||
return true, connectedUser.ConnectedTo
|
connected = true
|
||||||
|
connections = append(connections, connectedUser.ConnectedTo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false, ""
|
return connected, connections
|
||||||
}
|
}
|
||||||
|
|
||||||
func (oAdmin *OvpnAdmin) downloadCerts() bool {
|
func (oAdmin *OvpnAdmin) downloadCerts() bool {
|
||||||
|
|
152
package-lock.json
generated
Normal file
152
package-lock.json
generated
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
{
|
||||||
|
"requires": true,
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"dependencies": {
|
||||||
|
"@polka/url": {
|
||||||
|
"version": "1.0.0-next.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz",
|
||||||
|
"integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"acorn": {
|
||||||
|
"version": "8.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz",
|
||||||
|
"integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"acorn-walk": {
|
||||||
|
"version": "8.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
|
||||||
|
"integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"ansi-styles": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"color-convert": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"chalk": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"ansi-styles": "^4.1.0",
|
||||||
|
"supports-color": "^7.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"color-convert": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"color-name": "~1.1.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"color-name": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"commander": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"duplexer": {
|
||||||
|
"version": "0.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
|
||||||
|
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"gzip-size": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"duplexer": "^0.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"has-flag": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"lodash": {
|
||||||
|
"version": "4.17.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"mrmime": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-a70zx7zFfVO7XpnQ2IX1Myh9yY4UYvfld/dikWRnsXxbyvMcfz+u6UfgNAtH+k2QqtJuzVpv6eLTx1G2+WKZbQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"opener": {
|
||||||
|
"version": "1.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
|
||||||
|
"integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"sirv": {
|
||||||
|
"version": "1.0.19",
|
||||||
|
"resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.19.tgz",
|
||||||
|
"integrity": "sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@polka/url": "^1.0.0-next.20",
|
||||||
|
"mrmime": "^1.0.0",
|
||||||
|
"totalist": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"supports-color": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"has-flag": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"totalist": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"webpack-bundle-analyzer": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-GUMZlM3SKwS8Z+CKeIFx7CVoHn3dXFcUAjT/dcZQQmfSZGvitPfMob2ipjai7ovFFqPvTqkEZ/leL4O0YOdAYQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"acorn": "^8.0.4",
|
||||||
|
"acorn-walk": "^8.0.0",
|
||||||
|
"chalk": "^4.1.0",
|
||||||
|
"commander": "^7.2.0",
|
||||||
|
"gzip-size": "^6.0.0",
|
||||||
|
"lodash": "^4.17.20",
|
||||||
|
"opener": "^1.5.2",
|
||||||
|
"sirv": "^1.0.7",
|
||||||
|
"ws": "^7.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ws": {
|
||||||
|
"version": "7.5.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz",
|
||||||
|
"integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,7 +8,7 @@ env
|
||||||
auth_usr=$(head -1 $1)
|
auth_usr=$(head -1 $1)
|
||||||
auth_passwd=$(tail -1 $1)
|
auth_passwd=$(tail -1 $1)
|
||||||
|
|
||||||
if [ $common_name = $username ]; then
|
if [ $common_name = $auth_usr ]; then
|
||||||
openvpn-user auth --db.path /etc/openvpn/easyrsa/pki/users.db --user ${auth_usr} --password ${auth_passwd}
|
openvpn-user auth --db.path /etc/openvpn/easyrsa/pki/users.db --user ${auth_usr} --password ${auth_passwd}
|
||||||
else
|
else
|
||||||
echo "Authorization failed"
|
echo "Authorization failed"
|
||||||
|
|
|
@ -14,6 +14,7 @@ keepalive 10 60
|
||||||
persist-key
|
persist-key
|
||||||
persist-tun
|
persist-tun
|
||||||
topology subnet
|
topology subnet
|
||||||
|
#duplicate-cn
|
||||||
#proto tcp
|
#proto tcp
|
||||||
#port 1194
|
#port 1194
|
||||||
#dev tun0
|
#dev tun0
|
||||||
|
|
Loading…
Reference in a new issue