This commit is contained in:
Ilya Sosnovsky 2020-10-15 19:12:31 +03:00
parent 9f4c4e2c5c
commit 6ce657d587
11 changed files with 383 additions and 34 deletions

9
.dockerignore Normal file
View File

@ -0,0 +1,9 @@
# IntelliJ project files
.idea
*.iml
out
gen
./easyrsa
werf.yaml

31
.werffiles/configure.sh Normal file
View File

@ -0,0 +1,31 @@
#!/bin/bash
EASY_RSA_LOC="/etc/openvpn/easyrsa"
SERVER_CERT="${EASY_RSA_LOC}/pki/issued/server.crt"
cd $EASY_RSA_LOC
if [ -e "$SERVER_CERT" ]; then
echo "found existing certs - reusing"
else
cp -R /usr/share/easy-rsa/* $EASY_RSA_LOC
easyrsa init-pki
echo "ca\n" | easyrsa build-ca nopass
easyrsa build-server-full server nopass
easyrsa gen-dh
openvpn --genkey --secret ./pki/ta.key
fi
easyrsa gen-crl
iptables -t nat -A POSTROUTING -s 172.16.100.0/255.255.255.0 ! -d 172.16.100.0/255.255.255.0 -j MASQUERADE
mkdir -p /dev/net
if [ ! -c /dev/net/tun ]; then
mknod /dev/net/tun c 10 200
fi
cp -f /etc/openvpn/setup/openvpn.conf /etc/openvpn/openvpn.conf
[ -d /etc/openvpn/certs/pki ] && chmod 755 /etc/openvpn/certs/pki
[ -f /etc/openvpn/certs/pki/crl.pem ] && chmod 644 /etc/openvpn/certs/pki/crl.pem
openvpn --config /etc/openvpn/openvpn.conf --client-config-dir /etc/openvpn/ccd

17
Dockerfile Normal file
View File

@ -0,0 +1,17 @@
FROM golang:1.14.2-alpine3.11 AS backend-builder
COPY . /app
RUN apk --no-cache add build-base git gcc
RUN cd /app && env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags='-extldflags "-static" -s -w' -o openvpn-ui
FROM node:14.2-alpine3.11 AS frontend-builder
COPY frontend/ /app
RUN cd /app && npm install && npm run build
FROM alpine:3.11
WORKDIR /app
COPY --from=backend-builder /app/openvpn-ui /app
COPY --from=frontend-builder /app/static /app/static
RUN echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing/" >> /etc/apk/repositories && \
apk add --update bash easy-rsa && \
ln -s /usr/share/easy-rsa/easyrsa /usr/local/bin && \
rm -rf /tmp/* /var/tmp/* /var/cache/apk/* /var/cache/distfiles/*

23
docker-compose.yaml Normal file
View File

@ -0,0 +1,23 @@
version: '3'
volumes:
ovpn_data:
ovpn_config:
services:
openvpn:
image: openvpn:local
command: /etc/openvpn/setup/configure.sh
ports:
- 1194:1194
volumes:
- ovpn_data:/etc/openvpn/easyrsa
openvpn-admin:
build:
context: .
image: openvpn-admin:local
command: /app/openvpn-ui
ports:
- 8080:8080
volumes:
- ovpn_data:/mnt/easyrsa

View File

@ -34,7 +34,7 @@ new Vue({
ctxTop: '0',
ctxLeft: '0',
ctxVisible: false,
ctxMenuItems: { 'u-revoke': 'Revoke', 'u-unrevoke': 'Unrevoke', 'u-show-config': 'Show config'},
ctxMenuItems: { 'u-revoke': 'Revoke', 'u-unrevoke': 'Unrevoke', 'u-show-config': 'Show config', 'u-edit-ccd': "Edit routes"},
columns: [],
data: {},
name: '',
@ -81,6 +81,15 @@ new Vue({
_this.u.openvpnConfig = response.data;
});
})
this.$root.$on('u-edit-ccd', function () {
this.u.modalShowCcdVisible = true;
var data = new URLSearchParams();
data.append('username', _this.u.name);
axios.request(axios_cfg('api/user/ccd/list', data, 'form'))
.then(function(response) {
_this.u.ccds = response.data;
});
})
},
computed: {
uCtxStyle: function () {
@ -114,6 +123,13 @@ new Vue({
_this.u.data = response.data
});
},
u_get_ccd: function() {
var _this = this;
axios.request(axios_cfg('api/user/ccd'))
.then(function(response) {
_this.u.data = response.data
});
},
create_user: function() {
var _this = this;
var data = new URLSearchParams();
@ -124,6 +140,17 @@ new Vue({
_this.u_get_data();
_this.u.newUserName = '';
});
},
ccd_apply: function() {
var _this = this;
var data = new URLSearchParams();
data.append('username', this.u.newUserName);
axios.request(axios_cfg('api/user/ccd/apply', data, 'form'))
.then(function(response) {
console.log(response.data);
_this.u_get_data();
_this.u.newUserName = '';
});
}
}
})

View File

@ -57,6 +57,44 @@
</div>
</div>
<div class="modal-wrapper" v-if="u.modalShowCcdVisible">
<div class="modal-ccd-config">
<div class="row">
<table class="table table-bordered table-hover">
<thead>
<tr>
<th scope="col">Address</th>
<th scope="col">Mask</th>
<th scope="col">Description</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody>
<tr v-for="(route, index) in u.ccd">
<td>{{ route.addr }}</td>
<td>{{ route.mask }}</td>
<td>{{ route.desc }}</td>
<td>
<!-- button type="button" class="btn btn-primary btn-sm el-square" v-on:click.stop="ccd_edit()">Edit</button -->
<button type="button" class="btn btn-primary btn-sm el-square" v-on:click.stop="ccd.splice(index, 1)">Delete</button>
</td>
</tr>>
</tbody>
</table>
</div>
<div class="row">
<input type="text" v-model="route.addr" />
<input type="text" v-model="route.mask" />
<input type="text" v-model="route.desc" />
<button type="button" class="btn btn-success el-square" v-on:click.stop="ccd.push(route)">Add new route</button>
</div>
<div class="row">
<button type="button" class="btn btn-success el-square" v-on:click.stop="ccd_apply()">Save</button>
<button type="button" class="btn btn-danger el-square" v-on:click.stop="u.ccd=[];u.modalShowCcdVisible=false">Close</button>
</div>
</div>
</div>
</div>
<script src="dist/build.js"></script>
</body>

View File

@ -7,7 +7,7 @@ fi
cd easyrsa
if [ ! -f easyrsa ]; then
curl -sL https://github.com/OpenVPN/easy-rsa/releases/download/v3.0.6/EasyRSA-unix-v3.0.6.tgz | tar -xzv --strip-components=1 -C .
curl -sL https://github.com/OpenVPN/easy-rsa/releases/download/v3.0.8/EasyRSA-3.0.8.tgz | tar -xzv --strip-components=1 -C .
fi
if [ -d pki ]; then

6
go.mod
View File

@ -1,3 +1,9 @@
module openvpn-web-ui
go 1.14
require (
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect
gopkg.in/alecthomas/kingpin.v2 v2.2.6
)

12
go.sum Normal file
View File

@ -0,0 +1,12 @@
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

141
main.go
View File

@ -17,16 +17,23 @@ import (
// "encoding/binary"
"encoding/json"
"net/http"
"gopkg.in/alecthomas/kingpin.v2"
)
var (
listenHost = kingpin.Flag("listen.host","host for openvpn-admin").Default("127.0.0.1").String()
listenPort = kingpin.Flag("listen.port","port for openvpn-admin").Default("8080").String()
easyrsaPath = kingpin.Flag("easyrsa.path", "path to easyrsa dir").Default("/etc/openvpn/easyrsa").String()
indexTxtPath = kingpin.Flag("easyrsa.index-path", "path to easyrsa index file.").Default("/etc/openvpn/easyrsa/pki/index.txt").String()
ccdCustom = kingpin.Flag("ccd.custom", "enable or disable custom routes").Default("false").Bool()
ccdDir = kingpin.Flag("ccd.path", "path to client-config-dir").Default("/etc/openvpn/ccd").String()
staticPath = kingpin.Flag("static.path", "path to static dir").Default("./static").String()
)
const (
easyrsaPath = "easyrsa"
indexTxtPath = easyrsaPath + "/pki/index.txt"
usernameRegexp = `^([a-zA-Z0-9_.-])+$`
openvpnServerHost = "127.0.0.1"
openvpnServerPort = "7777"
listenHost = "127.0.0.1"
listenPort = "8080"
mgmtListenHost = "127.0.0.1"
mgmtListenPort = "7788"
)
@ -40,6 +47,16 @@ type openvpnClientConfig struct {
TLS string
}
type ccdLine struct {
addr string `json:"addr"`
mask string `json:"mask"`
desc string `json:"desc"`
}
type ccdFile struct {
lines []ccdLine
}
type indexTxtLine struct {
Flag string
ExpirationDate string
@ -63,7 +80,7 @@ type clientStatus struct {
}
func userListHandler(w http.ResponseWriter, r *http.Request) {
userList, _ := json.Marshal(indexTxtParser(fRead(indexTxtPath)))
userList, _ := json.Marshal(indexTxtParser(fRead(*indexTxtPath)))
fmt.Fprintf(w, "%s", userList)
}
@ -88,21 +105,24 @@ func userShowConfigHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s", renderClientConfig(r.FormValue("username")))
}
func userShowCcdHandler(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
fmt.Printf("username: %v\n%s\n", r.PostForm, r.FormValue("username"))
fmt.Fprintf(w, "%s", renderCcdConfig(r.FormValue("username")))
}
func userApplyCcdHandler(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
fmt.Printf("username: %v\n%s\n", r.PostForm, r.FormValue("username"))
fmt.Fprintf(w, "%s", ccdFileModify(r.FormValue("username"),ccdFileParser(r.FormValue("ccd"))))
}
func main() {
fmt.Println("Bind: http://" + listenHost + ":" + listenPort)
// usersFromIndexTxt := indexTxtParser(fRead(indexTxtPath))
// fRead(indexTxtPath)
// fWrite("hello", "hi")
// renderIndexTxt(indexTxtParser(fRead(indexTxtPath)))
// renderClientConfig("asd")
// crlFix()
// fmt.Println(reflect.TypeOf(indexTxtConf))
// fmt.Print(userUnrevoke("asd"))
// renderIndexTxt(usersFromIndexTxt)
// x := getActiveClients()
// fmt.Printf("%#v", x)
// killUserConnection("x")
fs := http.FileServer(http.Dir("./frontend/static"))
kingpin.Parse()
fmt.Println("Bind: http://" + *listenHost + ":" + *listenPort)
fs := http.FileServer(http.Dir(*staticPath))
http.Handle("/", fs)
http.HandleFunc("/api/users/list", userListHandler)
@ -110,7 +130,10 @@ func main() {
http.HandleFunc("/api/user/revoke", userRevokeHandler)
http.HandleFunc("/api/user/unrevoke", userUnrevokeHandler)
http.HandleFunc("/api/user/showconfig", userShowConfigHandler)
log.Fatal(http.ListenAndServe(listenHost+":"+listenPort, nil))
http.HandleFunc("/api/user/ccd/list", userShowCcdHandler)
http.HandleFunc("/api/user/ccd/apply", userApplyCcdHandler)
log.Fatal(http.ListenAndServe(*listenHost + ":" + *listenPort, nil))
}
func fRead(path string) string {
@ -175,10 +198,10 @@ func renderClientConfig(username string) string {
conf := openvpnClientConfig{}
conf.Host = openvpnServerHost
conf.Port = openvpnServerPort
conf.CA = fRead(easyrsaPath + "/pki/ca.crt")
conf.Cert = fRead(easyrsaPath + "/pki/issued/" + username + ".crt")
conf.Key = fRead(easyrsaPath + "/pki/private/" + username + ".key")
conf.TLS = fRead(easyrsaPath + "/pki/ta.key")
conf.CA = fRead(*easyrsaPath + "/pki/ca.crt")
conf.Cert = fRead(*easyrsaPath + "/pki/issued/" + username + ".crt")
conf.Key = fRead(*easyrsaPath + "/pki/private/" + username + ".key")
conf.TLS = fRead(*easyrsaPath + "/pki/ta.key")
t, _ := template.ParseFiles("client.conf.tpl")
var tmp bytes.Buffer
t.Execute(&tmp, conf)
@ -190,10 +213,47 @@ func renderClientConfig(username string) string {
return (fmt.Sprintf("User \"%s\" not found", username))
}
func ccdFileParser(txt string) ccdFile {
ccdFile := ccdFile{}
txtLinesArray := strings.Split(txt, "\n")
for _, v := range txtLinesArray {
str := strings.Fields(v)
if len(str) > 0 {
switch {
case strings.HasPrefix(str[0], "ifconfig-push"):
ccdFile.lines = append(ccdFile.lines, ccdLine{addr: str[2], mask: str[3], desc: "Client Address"})
case strings.HasPrefix(str[0], "push"):
ccdFile.lines = append(ccdFile.lines, ccdLine{addr: str[2], mask: str[3], desc: strings.Join(str[4:], "")})
}
}
}
return ccdFile
}
func renderCcdConfig(username string) string {
if checkCcdExist(username) {
ccdFileParser(fRead(*ccdDir + "/" + username))
}
fmt.Printf("ccd for user \"%s\" not found", username)
return (fmt.Sprintf("ccd for user \"%s\" not found", username))
}
func ccdFileModify(username string, ccdFile ccdFile) bool {
if checkCcdExist(username) {
}
return true
}
// https://community.openvpn.net/openvpn/ticket/623
func crlFix() {
os.Chmod(easyrsaPath+"/pki", 0755)
err := os.Chmod(easyrsaPath+"/pki/crl.pem", 0640)
os.Chmod(*easyrsaPath + "/pki", 0755)
err := os.Chmod(*easyrsaPath + "/pki/crl.pem", 0640)
if err != nil {
log.Println(err)
}
@ -215,7 +275,7 @@ func validateUsername(username string) bool {
}
func checkUserExist(username string) bool {
for _, u := range indexTxtParser(fRead(indexTxtPath)) {
for _, u := range indexTxtParser(fRead(*indexTxtPath)) {
if u.DistinguishedName == ("/CN=" + username) {
return (true)
}
@ -223,9 +283,26 @@ func checkUserExist(username string) bool {
return (false)
}
func checkCcdExist(username string) bool {
if *ccdCustom {
if _, err := os.Stat(*ccdDir + "/" + username); err == nil {
return (true)
} else if os.IsNotExist(err) {
fmt.Printf("ccd for user \"%s\" not found", username)
return (false)
} else {
fmt.Printf("Something goes wrong during checking ccd for user \"%s\"", username)
fmt.Printf("err: %s", err)
return (false)
}
}
return (false)
}
func usersList() []string {
users := []string{}
for _, line := range indexTxtParser(fRead(indexTxtPath)) {
for _, line := range indexTxtParser(fRead(*indexTxtPath)) {
users = append(users, line.Identity)
}
return (users)
@ -240,7 +317,7 @@ func userCreate(username string) string {
fmt.Printf("User \"%s\" already exists\n", username)
return (fmt.Sprintf("User \"%s\" already exists\n", username))
}
o := runBash(fmt.Sprintf("date +%%Y-%%m-%%d\\ %%H:%%M:%%S && cd %s && ./easyrsa build-client-full %s nopass", easyrsaPath, username))
o := runBash(fmt.Sprintf("date +%%Y-%%m-%%d\\ %%H:%%M:%%S && cd %s && ./easyrsa build-client-full %s nopass", *easyrsaPath, username))
fmt.Println(o)
return ("")
}
@ -248,7 +325,7 @@ func userCreate(username string) string {
func userRevoke(username string) string {
if checkUserExist(username) {
// check certificate valid flag 'V'
o := runBash(fmt.Sprintf("date +%%Y-%%m-%%d\\ %%H:%%M:%%S && cd %s && echo yes | ./easyrsa revoke %s && ./easyrsa gen-crl", easyrsaPath, username))
o := runBash(fmt.Sprintf("date +%%Y-%%m-%%d\\ %%H:%%M:%%S && cd %s && echo yes | ./easyrsa revoke %s && ./easyrsa gen-crl", *easyrsaPath, username))
crlFix()
return (fmt.Sprintln(o))
}
@ -259,7 +336,7 @@ func userRevoke(username string) string {
func userUnrevoke(username string) string {
if checkUserExist(username) {
// check certificate revoked flag 'R'
usersFromIndexTxt := indexTxtParser(fRead(indexTxtPath))
usersFromIndexTxt := indexTxtParser(fRead(*indexTxtPath))
for i := range usersFromIndexTxt {
if usersFromIndexTxt[i].DistinguishedName == ("/CN=" + username) {
usersFromIndexTxt[i].Flag = "V"
@ -267,7 +344,7 @@ func userUnrevoke(username string) string {
break
}
}
fWrite(indexTxtPath, renderIndexTxt(usersFromIndexTxt))
fWrite(*indexTxtPath, renderIndexTxt(usersFromIndexTxt))
fmt.Print(renderIndexTxt(usersFromIndexTxt))
crlFix()
return (fmt.Sprintf("{\"msg\":\"User %s successfully unrevoked\"}", username))

109
werf.yaml Normal file
View File

@ -0,0 +1,109 @@
project: openvpn-web-ui
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-ui
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-ui
to: /usr/bin/openvpn-ui
before: setup
- artifact: frontend-builder
add: /app/static
to: /app/static
before: 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/configure.sh
to: /etc/openvpn/setup/configure.sh
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