diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..bcaf47c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +# IntelliJ project files +.idea +*.iml +out +gen + + +./easyrsa +werf.yaml diff --git a/.werffiles/configure.sh b/.werffiles/configure.sh new file mode 100644 index 0000000..0733c15 --- /dev/null +++ b/.werffiles/configure.sh @@ -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 + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a776f4e --- /dev/null +++ b/Dockerfile @@ -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/* diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..4b6eb53 --- /dev/null +++ b/docker-compose.yaml @@ -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 \ No newline at end of file diff --git a/frontend/src/main.js b/frontend/src/main.js index e782997..7ac1a59 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -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 = ''; + }); } } }) diff --git a/frontend/static/index.html b/frontend/static/index.html index cc52368..5c08fcc 100644 --- a/frontend/static/index.html +++ b/frontend/static/index.html @@ -57,6 +57,44 @@ + + diff --git a/get-easyrsa-end-gen-certs.sh b/get-easyrsa-end-gen-certs.sh index 356b8ce..38244fd 100755 --- a/get-easyrsa-end-gen-certs.sh +++ b/get-easyrsa-end-gen-certs.sh @@ -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 diff --git a/go.mod b/go.mod index 9990ad9..fdd68e8 100644 --- a/go.mod +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..43073c4 --- /dev/null +++ b/go.sum @@ -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= diff --git a/main.go b/main.go index 569f545..a76b357 100644 --- a/main.go +++ b/main.go @@ -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)) diff --git a/werf.yaml b/werf.yaml new file mode 100644 index 0000000..73e594b --- /dev/null +++ b/werf.yaml @@ -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 + +