Additional password auth; Multiple mgmt interface usgae; Fixes; style changes;

This commit is contained in:
Ilya Sosnovsky 2021-02-20 15:48:41 +03:00
parent bec0e738d1
commit 3614ab6ba5
14 changed files with 57 additions and 156 deletions

View File

@ -1,22 +1,18 @@
FROM golang:1.14.2-alpine3.11 AS backend-builder FROM golang:1.14.2-buster AS backend-builder
COPY . /app COPY . /app
#RUN apk --no-cache add build-base git gcc RUN cd /app && env CGO_ENABLED=./1 GOOS=linux GOARCH=amd64 go build -ldflags='-linkmode external -extldflags "-static" -s -w' -o openvpn-admin
RUN cd /app && env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags='-extldflags "-static" -s -w' -o openvpn-admin
FROM node:14.2-alpine3.11 AS frontend-builder FROM node:14.2-alpine3.11 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
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 FROM alpine:3.13
WORKDIR /app WORKDIR /app
COPY --from=backend-builder /app/openvpn-admin /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 --from=frontend-builder /app/static /app/static
COPY client.conf.tpl /app/client.conf.tpl COPY client.conf.tpl /app/client.conf.tpl
COPY ccd.tpl /app/ccd.tpl COPY ccd.tpl /app/ccd.tpl
RUN apk add --update bash easy-rsa && \ RUN apk add --update bash easy-rsa && \
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-rc.1/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/*

View File

@ -1,10 +1,7 @@
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 FROM alpine:3.13
COPY --from=user-builder /app/openvpn-user /usr/local/bin
RUN apk add --update bash openvpn easy-rsa && \ RUN apk add --update bash openvpn easy-rsa && \
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-rc.1/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 .werffiles /etc/openvpn/setup COPY setup/ /etc/openvpn/setup
RUN chmod +x /etc/openvpn/setup/configure.sh RUN chmod +x /etc/openvpn/setup/configure.sh

View File

@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags='-extldflags "-static" -s -w' -o openvpn-admin CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -ldflags "-linkmode external -extldflags -static -s -w" -o openvpn-admin

View File

@ -1,5 +1,5 @@
{{- range $server := .Hosts }} {{- range $server := .Hosts }}
remote {{ $server.Host }} {{ $server.Port }} tcp remote {{ $server.Host }} {{ $server.Port }} {{ $server.Protocol }}
{{- end }} {{- end }}
verb 4 verb 4

View File

@ -22,7 +22,7 @@ services:
build: build:
context: . context: .
image: openvpn-admin:local 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" --auth.password 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.server="127.0.0.1:7744" --ovpn.server="127.0.0.1:7778" --auth.password
environment: environment:
- OPVN_SLAVE=1 - OPVN_SLAVE=1
network_mode: service:openvpn network_mode: service:openvpn

View File

@ -7838,6 +7838,11 @@
"vue-style-loader": "^4.1.0" "vue-style-loader": "^4.1.0"
} }
}, },
"vue-notification": {
"version": "1.3.20",
"resolved": "https://registry.npmjs.org/vue-notification/-/vue-notification-1.3.20.tgz",
"integrity": "sha512-vPj67Ah72p8xvtyVE8emfadqVWguOScAjt6OJDEUdcW5hW189NsqvfkOrctxHUUO9UYl9cTbIkzAEcPnHu+zBQ=="
},
"vue-style-loader": { "vue-style-loader": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.2.tgz", "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.2.tgz",

View File

@ -14,7 +14,8 @@
"vue": "^2.6.12", "vue": "^2.6.12",
"vue-clipboard2": "^0.2.1", "vue-clipboard2": "^0.2.1",
"vue-cookies": "^1.7.4", "vue-cookies": "^1.7.4",
"vue-good-table": "^2.21.1" "vue-good-table": "^2.21.1",
"vue-notification": "^1.3.20"
}, },
"browserslist": [ "browserslist": [
"> 1%", "> 1%",

View File

@ -3,12 +3,14 @@ import axios from 'axios';
import VueCookies from 'vue-cookies' import VueCookies from 'vue-cookies'
import VueClipboard from 'vue-clipboard2' import VueClipboard from 'vue-clipboard2'
import VueGoodTablePlugin from 'vue-good-table' import VueGoodTablePlugin from 'vue-good-table'
import Notifications from 'vue-notification'
import 'vue-good-table/dist/vue-good-table.css' import 'vue-good-table/dist/vue-good-table.css'
Vue.use(VueClipboard) Vue.use(VueClipboard)
Vue.use(VueGoodTablePlugin) Vue.use(VueGoodTablePlugin)
Vue.use(VueCookies) Vue.use(VueCookies)
Vue.use(Notifications)
var axios_cfg = function(url, data='', type='form') { var axios_cfg = function(url, data='', type='form') {
if (data == '') { if (data == '') {
@ -182,6 +184,7 @@ new Vue({
axios.request(axios_cfg('api/user/revoke', data, 'form')) axios.request(axios_cfg('api/user/revoke', data, 'form'))
.then(function(response) { .then(function(response) {
_this.getUserData(); _this.getUserData();
_this.$notify({title: 'User ' + _this.username + ' revoked!', type: 'warn'})
}); });
}) })
_this.$root.$on('u-unrevoke', function () { _this.$root.$on('u-unrevoke', function () {
@ -190,6 +193,7 @@ new Vue({
axios.request(axios_cfg('api/user/unrevoke', data, 'form')) axios.request(axios_cfg('api/user/unrevoke', data, 'form'))
.then(function(response) { .then(function(response) {
_this.getUserData(); _this.getUserData();
_this.$notify({title: 'User ' + _this.username + ' unrevoked!', type: 'success'})
}); });
}) })
_this.$root.$on('u-show-config', function () { _this.$root.$on('u-show-config', function () {
@ -289,6 +293,15 @@ new Vue({
_this.rows = response.data; _this.rows = response.data;
}); });
}, },
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";
},
getServerRole: function() { getServerRole: function() {
var _this = this; var _this = this;
axios.request(axios_cfg('api/server/role')) axios.request(axios_cfg('api/server/role'))
@ -318,9 +331,12 @@ new Vue({
_this.u.newUserName = ''; _this.u.newUserName = '';
_this.u.newUserPassword = ''; _this.u.newUserPassword = '';
_this.getUserData(); _this.getUserData();
_this.$notify({title: 'New user ' + _this.username + ' created', type: 'success'})
}) })
.catch(function(error) { .catch(function(error) {
_this.u.newUserCreateError = error.response.data; _this.u.newUserCreateError = error.response.data;
_this.$notify({title: 'New user ' + _this.username + ' creation failed.', type: 'error'})
}); });
}, },
@ -334,10 +350,12 @@ new Vue({
.then(function(response) { .then(function(response) {
_this.u.ccdApplyStatus = 200; _this.u.ccdApplyStatus = 200;
_this.u.ccdApplyStatusMessage = response.data; _this.u.ccdApplyStatusMessage = response.data;
_this.$notify({title: 'Ccd for user ' + _this.username + ' applied', type: 'success'})
}) })
.catch(function(error) { .catch(function(error) {
_this.u.ccdApplyStatus = error.response.status; _this.u.ccdApplyStatus = error.response.status;
_this.u.ccdApplyStatusMessage = error.response.data; _this.u.ccdApplyStatusMessage = error.response.data;
_this.$notify({title: 'Ccd for user ' + _this.username + ' apply failed ', type: 'error'})
}); });
}, },
@ -354,13 +372,14 @@ new Vue({
.then(function(response) { .then(function(response) {
_this.u.passwordChangeStatus = 200; _this.u.passwordChangeStatus = 200;
_this.u.newPassword = ''; _this.u.newPassword = '';
_this.u.passwordChangeMessage = response.data.message;
_this.getUserData(); _this.getUserData();
_this.u.modalChangePasswordVisible = false; _this.u.modalChangePasswordVisible = false;
_this.$notify({title: 'Password for user ' + _this.username + ' changed!', type: 'success'})
}) })
.catch(function(error) { .catch(function(error) {
_this.u.passwordChangeStatus = error.response.status; _this.u.passwordChangeStatus = error.response.status;
_this.u.passwordChangeMessage = error.response.data.message; _this.u.passwordChangeMessage = error.response.data.message;
_this.$notify({title: 'Changing password for user ' + _this.username + ' failed!', type: 'error'})
}); });
}, },
} }

View File

@ -89,7 +89,7 @@
</div> </div>
<div class="modal-footer"> <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-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> <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.passwordChangeMessage='';u.modalChangePasswordVisible=false">Close</button>
</div> </div>
</div> </div>
</div> </div>
@ -127,10 +127,10 @@
<h5 class="static-address-label ">Static address:</h5> <h5 class="static-address-label ">Static address:</h5>
<div class="input-group-prepend"> <div class="input-group-prepend">
<div class="input-group-text"> <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" @change="staticAddrCheckboxOnChange()" v-if="serverRole == 'master'" v-bind:checked="!customAddressDisabled">
</div> </div>
</div> </div>
<input id="staticAddress" type="text" class="form-control" v-model="u.ccd.ClientAddress" placeholder="127.0.0.1" v-bind:disabled="customAddressDisabled"> <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>
<div class="modal-body"> <div class="modal-body">
@ -147,22 +147,22 @@
<tbody> <tbody>
<tr v-for="(customRoute, index) in u.ccd.CustomRoutes"> <tr v-for="(customRoute, index) in u.ccd.CustomRoutes">
<td> <td>
<div v-if = "serverRole == 'slave'"> <div v-if="serverRole == 'slave'">
{{ customRoute.Address }} {{ customRoute.Address }}
</div> </div>
<input v-if = "serverRole == 'master'" v-model = "customRoute.Address"> <input v-if="serverRole == 'master'" v-model="customRoute.Address">
</td> </td>
<td> <td>
<div v-if = "serverRole == 'slave'"> <div v-if="serverRole == 'slave'">
{{ customRoute.Mask }} {{ customRoute.Mask }}
</div> </div>
<input v-if = "serverRole == 'master'" v-model = "customRoute.Mask"> <input v-if="serverRole == 'master'" v-model="customRoute.Mask">
</td> </td>
<td> <td>
<div v-if = "serverRole == 'slave'"> <div v-if="serverRole == 'slave'">
{{ customRoute.Description }} {{ customRoute.Description }}
</div> </div>
<input v-if = "serverRole == 'master'" v-model = "customRoute.Description"> <input v-if="serverRole == 'master'" v-model="customRoute.Description">
</td> </td>
<td class="text-right" v-if="serverRole == 'master'"> <td class="text-right" v-if="serverRole == 'master'">
<button type="button" class="btn btn-danger btn-sm el-square modal-el-margin" v-if="serverRole == 'master'" v-on:click.stop="u.ccd.CustomRoutes.splice(index, 1)">Delete</button> <button type="button" class="btn btn-danger btn-sm el-square modal-el-margin" v-if="serverRole == 'master'" v-on:click.stop="u.ccd.CustomRoutes.splice(index, 1)">Delete</button>
@ -193,6 +193,7 @@
</div> </div>
</div> </div>
<notifications position="bottom left" :speed="900" />
</div> </div>
<script src="dist/build.js"></script> <script src="dist/build.js"></script>
</body> </body>

22
main.go
View File

@ -30,10 +30,10 @@ const (
indexTxtDateLayout = "060102150405Z" indexTxtDateLayout = "060102150405Z"
stringDateFormat = "2006-01-02 15:04:05" stringDateFormat = "2006-01-02 15:04:05"
ovpnStatusDateLayout = "Mon Jan 2 15:04:05 2006" ovpnStatusDateLayout = "Mon Jan 2 15:04:05 2006"
version = "1.5.0"
) )
var ( var (
listenHost = kingpin.Flag("listen.host","host for openvpn-admin").Default("0.0.0.0").String() listenHost = kingpin.Flag("listen.host","host for openvpn-admin").Default("0.0.0.0").String()
listenPort = kingpin.Flag("listen.port","port for openvpn-admin").Default("8080").String() listenPort = kingpin.Flag("listen.port","port for openvpn-admin").Default("8080").String()
serverRole = kingpin.Flag("role","server role master or slave").Default("master").HintOptions("master", "slave").String() serverRole = kingpin.Flag("role","server role master or slave").Default("master").HintOptions("master", "slave").String()
@ -42,10 +42,9 @@ var (
masterBasicAuthPassword = kingpin.Flag("master.basic-auth.password","password for basic auth on master server url").Default("").String() masterBasicAuthPassword = kingpin.Flag("master.basic-auth.password","password for basic auth on master server url").Default("").String()
masterSyncFrequency = kingpin.Flag("master.sync-frequency", "master host data sync frequency in seconds.").Default("600").Int() masterSyncFrequency = kingpin.Flag("master.sync-frequency", "master host data sync frequency in seconds.").Default("600").Int()
masterSyncToken = kingpin.Flag("master.sync-token", "master host data sync security token").Default("justasimpleword").PlaceHolder("TOKEN").String() 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() openvpnServer = kingpin.Flag("ovpn.server","comma separated addresses for openvpn servers").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() openvpnNetwork = kingpin.Flag("ovpn.network","network for openvpn server").Default("172.16.100.0/24").String()
mgmtAddress = kingpin.Flag("mgmt","comma separated (alias=addresses) for openvpn servers mgmt interfaces").Default("main=127.0.0.1:8989").Strings() mgmtAddress = kingpin.Flag("mgmt","comma separated (alias=address) 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() 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() 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() indexTxtPath = kingpin.Flag("easyrsa.index-path", "path to easyrsa index file.").Default("/mnt/easyrsa/pki/index.txt").String()
@ -152,6 +151,7 @@ type OpenvpnAdmin struct {
type OpenvpnServer struct { type OpenvpnServer struct {
Host string Host string
Port string Port string
Protocol string
} }
type openvpnClientConfig struct { type openvpnClientConfig struct {
@ -366,7 +366,9 @@ func (oAdmin *OpenvpnAdmin) downloadCcdHandler(w http.ResponseWriter, r *http.Re
} }
func main() { func main() {
kingpin.Parse() kingpin.Version(version)
kingpin.Parse()
ovpnAdmin := new(OpenvpnAdmin) ovpnAdmin := new(OpenvpnAdmin)
ovpnAdmin.lastSyncTime = "unknown" ovpnAdmin.lastSyncTime = "unknown"
@ -506,8 +508,8 @@ func (oAdmin *OpenvpnAdmin) renderClientConfig(username string) string {
var hosts []OpenvpnServer var hosts []OpenvpnServer
for _, server := range *openvpnServer { for _, server := range *openvpnServer {
parts := strings.SplitN(server, ":",2) parts := strings.SplitN(server, ":",3)
hosts = append(hosts, OpenvpnServer{Host: parts[0], Port: parts[1]}) hosts = append(hosts, OpenvpnServer{Host: parts[0], Port: parts[1], Protocol: parts[2]})
} }
conf := openvpnClientConfig{} conf := openvpnClientConfig{}
@ -947,7 +949,7 @@ func (oAdmin *OpenvpnAdmin) mgmtConnectedUsersParser(text, serverName string) []
func (oAdmin *OpenvpnAdmin) mgmtKillUserConnection(username, serverName string) { func (oAdmin *OpenvpnAdmin) mgmtKillUserConnection(username, serverName string) {
conn, err := net.Dial("tcp", oAdmin.mgmtInterfaces[serverName]) conn, err := net.Dial("tcp", oAdmin.mgmtInterfaces[serverName])
if err != nil { if err != nil {
log.Println("ERROR: openvpn mgmt interface is not reachable") log.Printf("WARNING: openvpn mgmt interface for %s is not reachable by addr %s\n", serverName, oAdmin.mgmtInterfaces[serverName])
return return
} }
oAdmin.mgmtRead(conn) // read welcome message oAdmin.mgmtRead(conn) // read welcome message
@ -962,8 +964,8 @@ func (oAdmin *OpenvpnAdmin) mgmtGetActiveClients() []clientStatus {
for srv, addr := range oAdmin.mgmtInterfaces { for srv, addr := range oAdmin.mgmtInterfaces {
conn, err := net.Dial("tcp", addr) conn, err := net.Dial("tcp", addr)
if err != nil { if err != nil {
log.Printf("ERROR: openvpn mgmt interface for %s is not reachable by addr %s\n", srv, addr) log.Printf("WARNING: openvpn mgmt interface for %s is not reachable by addr %s\n", srv, addr)
//return []clientStatus{} break
} }
oAdmin.mgmtRead(conn) // read welcome message oAdmin.mgmtRead(conn) // read welcome message
conn.Write([]byte("status\n")) conn.Write([]byte("status\n"))

120
werf.yaml
View File

@ -1,120 +0,0 @@
project: openvpn-admin
configVersion: 1
deploy:
helmRelease: "[[ project ]]-[[ env ]]"
namespace: "[[ project ]]-[[ env ]]"
---
artifact: backend-builder
from: golang:1.14.2-alpine3.11
git:
- add: /
to: /app
stageDependencies:
install:
- "*.go"
excludePaths:
- .helm
- .werf
- frontend
- werf.yaml
- Dockerfile
ansible:
install:
- name: Install packages
apk:
name:
- build-base
- gcc
- name: Build backend
command: go build -ldflags='-extldflags "-static" -s -w' -o openvpn-admin
environment:
CGO_ENABLED: 0
GOOS: linux
GOARCH: amd64
args:
chdir: /app
---
artifact: frontend-builder
from: node:14.2-alpine3.11
git:
- add: /frontend
to: /app
stageDependencies:
install:
- "**/*"
excludePaths:
- Dockerfile
- build.sh
- werf.yaml
ansible:
setup:
- name: install deps
command: npm install
args:
chdir: /app
- name: Build app
command: npm run build
args:
chdir: /app
---
image: openvpn-admin
from: alpine:3.11
import:
- artifact: backend-builder
add: /app/openvpn-admin
to: /usr/bin/openvpn-admin
before: setup
- artifact: frontend-builder
add: /app/static
to: /app/static
before: setup
git:
- add: /client.conf.tpl
to: /app/client.conf.tpl
stageDependencies:
setup:
- "*"
- add: /ccd.tpl
to: /app/ccd.tpl
stageDependencies:
setup:
- "*"
ansible:
install:
- name: Install packages
apk:
name:
- easy-rsa
- bash
- name: Create symbolic link for easy-rsa
file:
src: "/usr/share/easy-rsa/easyrsa"
dest: "/usr/local/bin/easyrsa"
state: link
---
image: openvpn
from: alpine:3.11
git:
- add: /.werffiles/
to: /etc/openvpn/setup/
stageDependencies:
install:
- "*"
ansible:
install:
- name: Install packages
apk:
name:
- openvpn
- easy-rsa
- name: Create symbolic link for easy-rsa
file:
src: "/usr/share/easy-rsa/easyrsa"
dest: "/usr/local/bin/easyrsa"
state: link