1
0
Эх сурвалжийг харах

WiP: basic implementation

Mustafa Arici 8 жил өмнө
parent
commit
95b41e2f10
12 өөрчлөгдсөн 1511 нэмэгдсэн , 0 устгасан
  1. 1 0
      .gitignore
  2. 319 0
      cmd/ovpm/main.go
  3. 25 0
      conf.go
  4. 76 0
      db.go
  5. 205 0
      pki.go
  6. 2 0
      template/ccd.file.tmpl
  7. 21 0
      template/client.ovpn.tmpl
  8. 24 0
      template/dh4096.pem.tmpl
  9. 0 0
      template/iptables.tmpl
  10. 287 0
      template/server.conf.tmpl
  11. 142 0
      user.go
  12. 409 0
      vpn.go

+ 1 - 0
.gitignore

@@ -1,3 +1,4 @@
+bindata.go
 compress
 data_import/
 *.db

+ 319 - 0
cmd/ovpm/main.go

@@ -0,0 +1,319 @@
+//go:generate go-bindata template/
+package main
+
+import (
+	"fmt"
+	"github.com/Sirupsen/logrus"
+	"github.com/cad/ovpm"
+	"github.com/olekukonko/tablewriter"
+	"github.com/urfave/cli"
+	"os"
+	"time"
+)
+
+var action string
+
+func main() {
+	app := cli.NewApp()
+	app.Name = "ovpm"
+	app.Usage = "OpenVPN Manager"
+	app.Version = ovpm.Version
+	app.Flags = []cli.Flag{
+		cli.BoolFlag{
+			Name:  "verbose",
+			Usage: "verbose output",
+		},
+	}
+	app.Before = func(c *cli.Context) error {
+		logrus.SetLevel(logrus.WarnLevel)
+		if c.GlobalBool("verbose") {
+			logrus.SetLevel(logrus.DebugLevel)
+		}
+		return nil
+	}
+	app.Commands = []cli.Command{
+		{
+			Name:  "user",
+			Usage: "User Operations",
+			Subcommands: []cli.Command{
+				{
+					Name:  "list",
+					Usage: "List VPN users.",
+					Action: func(c *cli.Context) error {
+						action = "user:list"
+						server, err := ovpm.GetServerInstance()
+						if err != nil {
+							os.Exit(1)
+							return err
+						}
+						users, err := ovpm.GetAllUsers()
+						if err != nil {
+							logrus.Errorf("users can not be fetched: %v", err)
+							os.Exit(1)
+							return err
+						}
+						table := tablewriter.NewWriter(os.Stdout)
+						table.SetHeader([]string{"#", "username", "created at", "valid crt"})
+						//table.SetBorder(false)
+						for i, user := range users {
+							data := []string{fmt.Sprintf("%v", i+1), user.Username, user.CreatedAt.Format(time.UnixDate), fmt.Sprintf("%t", server.CheckSerial(user.ServerSerialNumber))}
+							table.Append(data)
+						}
+						table.Render()
+
+						return nil
+					},
+				},
+				{
+					Name:  "create",
+					Usage: "Create a VPN user.",
+					Flags: []cli.Flag{
+						cli.StringFlag{
+							Name:  "username, u",
+							Usage: "username for the vpn user",
+						},
+						cli.StringFlag{
+							Name:  "password, p",
+							Usage: "password for the vpn user",
+						},
+					},
+					Action: func(c *cli.Context) error {
+						action = "user:create"
+						username := c.String("username")
+						password := c.String("password")
+
+						if username == "" || password == "" {
+							fmt.Println(cli.ShowSubcommandHelp(c))
+							os.Exit(1)
+						}
+						user, err := ovpm.CreateUser(username, password)
+						if err != nil {
+							logrus.Errorf("user can not be created '%s': %v", username, err)
+							os.Exit(1)
+							return err
+						}
+						logrus.Infof("user created: %s", user.Username)
+						return nil
+					},
+				},
+				{
+					Name:  "delete",
+					Usage: "Delete a VPN user.",
+					Flags: []cli.Flag{
+						cli.StringFlag{
+							Name:  "user, u",
+							Usage: "username of the vpn user",
+						},
+					},
+					Action: func(c *cli.Context) error {
+						action = "user:delete"
+						username := c.String("user")
+
+						if username == "" {
+							fmt.Println(cli.ShowSubcommandHelp(c))
+							os.Exit(1)
+						}
+						err := ovpm.DeleteUser(username)
+						if err != nil {
+							logrus.Errorf("user can not be deleted '%s': %v", username, err)
+							os.Exit(1)
+							return err
+						}
+						logrus.Infof("user deleted: %s", username)
+						return nil
+					},
+				},
+				{
+					Name:  "renew",
+					Usage: "Renew VPN user certificates.",
+					Flags: []cli.Flag{
+						cli.StringFlag{
+							Name:  "user, u",
+							Usage: "username of the vpn user",
+						},
+					},
+					Action: func(c *cli.Context) error {
+						action = "user:renew"
+						username := c.String("user")
+
+						if username == "" {
+							fmt.Println(cli.ShowSubcommandHelp(c))
+							os.Exit(1)
+						}
+						err := ovpm.SignUser(username)
+						if err != nil {
+							logrus.Errorf("can't renew user cert '%s': %v", username, err)
+							os.Exit(1)
+							return err
+						}
+						logrus.Infof("user certs renewed: '%s'", username)
+						return nil
+					},
+				},
+				{
+					Name:  "genconfig",
+					Usage: "Generate client config for the user. (.ovpn file)",
+					Flags: []cli.Flag{
+						cli.StringFlag{
+							Name:  "user, u",
+							Usage: "username of the vpn user",
+						},
+						cli.StringFlag{
+							Name:  "out, o",
+							Usage: ".ovpn file output path",
+						},
+					},
+					Action: func(c *cli.Context) error {
+						action = "user:export-config"
+						username := c.String("user")
+						output := c.String("out")
+
+						if username == "" {
+							fmt.Println(cli.ShowSubcommandHelp(c))
+							os.Exit(1)
+						}
+						if output == "" {
+							output = username + ".ovpn"
+						}
+						err := ovpm.DumpUserOVPNConf(username, output)
+						if err != nil {
+							logrus.Errorf("user config can not be exported %s: %v", username, err)
+							return err
+						}
+						fmt.Printf("exported to %s", output)
+						return nil
+					},
+				},
+				// {
+				// 	Name:  "lock",
+				// 	Usage: "Lock VPN user",
+				// 	Action: func(c *cli.Context) error {
+				// 		return nil
+				// 	},
+				// },
+				// {
+				// 	Name:  "unlock",
+				// 	Usage: "Unlock VPN user",
+				// 	Action: func(c *cli.Context) error {
+				// 		return nil
+				// 	},
+				// },
+			},
+		},
+		{
+			Name:  "vpn",
+			Usage: "VPN Operations",
+			Subcommands: []cli.Command{
+				{
+					Name:  "status",
+					Usage: "Show VPN status.",
+					Action: func(c *cli.Context) error {
+						server, err := ovpm.GetServerInstance()
+						if err != nil {
+							os.Exit(1)
+							return err
+						}
+
+						table := tablewriter.NewWriter(os.Stdout)
+						table.SetHeader([]string{"attribute", "value"})
+						table.Append([]string{"Name", server.Name})
+						table.Append([]string{"Hostname", server.Hostname})
+						table.Append([]string{"Port", server.Port})
+						table.Append([]string{"Network", server.Net})
+						table.Append([]string{"Netmask", server.Mask})
+						table.Append([]string{"Created At", server.CreatedAt.Format(time.UnixDate)})
+						table.Render()
+
+						return nil
+					},
+				},
+				{
+					Name:  "init",
+					Usage: "Initialize VPN server.",
+					Flags: []cli.Flag{
+						cli.StringFlag{
+							Name:  "hostname, s",
+							Usage: "ip address or FQDN of the vpn server",
+						},
+						cli.StringFlag{
+							Name:  "port, p",
+							Usage: "port number of the vpn server",
+							Value: ovpm.DefaultVPNPort,
+						},
+					},
+					Action: func(c *cli.Context) error {
+						action = "vpn:init"
+						if c.String("hostname") == "" {
+							logrus.Errorf("'hostname' is needed")
+							fmt.Println(cli.ShowSubcommandHelp(c))
+							os.Exit(1)
+
+						}
+
+						if ovpm.CheckBootstrapped() {
+							var response string
+							for {
+								fmt.Println("This operation will cause invalidation of existing user certificates.")
+								fmt.Println("After this opeartion, new client config files (.ovpn) should be generated for each existing user.")
+								fmt.Println()
+								fmt.Println("Are you sure ? (y/N)")
+								_, err := fmt.Scanln(&response)
+								if err != nil {
+									logrus.Fatal(err)
+									os.Exit(1)
+									return err
+								}
+								okayResponses := []string{"y", "Y", "yes", "Yes", "YES"}
+								nokayResponses := []string{"n", "N", "no", "No", "NO"}
+								if stringInSlice(response, okayResponses) {
+									if err := ovpm.DeleteServer("default"); err != nil {
+										logrus.Errorf("server can not be deleted: %v", err)
+										os.Exit(1)
+										return err
+									}
+
+									break
+								} else if stringInSlice(response, nokayResponses) {
+									return fmt.Errorf("user decided to cancel")
+								}
+							}
+
+						}
+						if err := ovpm.CreateServer("default", c.String("hostname"), c.String("port")); err != nil {
+							logrus.Errorf("server can not be created: %v", err)
+							fmt.Println(cli.ShowSubcommandHelp(c))
+							os.Exit(1)
+						}
+
+						return nil
+					},
+				},
+				{
+					Name:  "apply",
+					Usage: "Apply pending changes.",
+					Action: func(c *cli.Context) error {
+						action = "apply"
+						if err := ovpm.Emit(); err != nil {
+							logrus.Errorf("can not apply configuration: %v", err)
+							os.Exit(1)
+							return err
+						}
+						logrus.Info("changes applied")
+						return nil
+					},
+				},
+			},
+		},
+	}
+	app.Run(os.Args)
+	ovpm.CloseDB()
+}
+
+func stringInSlice(a string, list []string) bool {
+	for _, b := range list {
+		if b == a {
+			return true
+		}
+	}
+	return false
+}

+ 25 - 0
conf.go

@@ -0,0 +1,25 @@
+package ovpm
+
+const (
+	// Version defines the version of ovpm.
+	Version = "0.0.0"
+
+	etcBasePath         = "/etc/ovpm/"
+	varBasePath         = "/var/db/ovpm/"
+	DefaultConfigPath   = etcBasePath + "ovpm.ini"
+	DefaultDBPath       = varBasePath + "db.sqlite3"
+	DefaultVPNConfPath  = varBasePath + "server.conf"
+	DefaultVPNPort      = "1197"
+	DefaultVPNCCDPath   = varBasePath + "ccd/"
+	DefaultCertPath     = varBasePath + "server.crt"
+	DefaultKeyPath      = varBasePath + "server.key"
+	DefaultCACertPath   = varBasePath + "ca.crt"
+	DefaultCAKeyPath    = varBasePath + "ca.key"
+	DefaultDHParamsPath = varBasePath + "dh4096.pem"
+
+	CrtExpireYears = 10
+	CrtKeyLength   = 2024
+
+	DefaultServerNetwork = "10.9.0.0"
+	DefaultServerNetMask = "255.255.255.0"
+)

+ 76 - 0
db.go

@@ -0,0 +1,76 @@
+package ovpm
+
+import (
+	"github.com/Sirupsen/logrus"
+	"github.com/jinzhu/gorm"
+	_ "github.com/jinzhu/gorm/dialects/sqlite"
+)
+
+var db *gorm.DB
+
+// User is database model for VPN users.
+type User struct {
+	gorm.Model
+	ServerID           uint
+	Server             Server
+	ServerSerialNumber string
+
+	Username string `gorm:"unique_index"`
+	Password string
+	Cert     string
+	Key      string
+}
+
+func (u *User) setPassword(newPassword string) error {
+	// TODO(cad): Use a proper password hashing algorithm here.
+	u.Password = newPassword
+	return nil
+}
+
+// Network is database model for external networks on the VPN server.
+type Network struct {
+	gorm.Model
+	ServerID uint
+	Server   Server
+
+	Name        string
+	NetworkCIDR string
+}
+
+// Server is database model for storing VPN server related stuff.
+type Server struct {
+	gorm.Model
+	Name         string `gorm:"unique_index"` // Server name.
+	SerialNumber string
+
+	Hostname string // Server's ip address or FQDN
+	Port     string // Server's listening port
+	Cert     string // Server RSA certificate.
+	Key      string // Server RSA private key.
+	CACert   string // Root CA RSA certificate.
+	CAKey    string // Root CA RSA key.
+	Net      string // VPN network.
+	Mask     string // VPN network mask.
+}
+
+// CheckSerial takes a serial number and checks it against the current server's serial number.
+func (s *Server) CheckSerial(serialNo string) bool {
+	return serialNo == s.SerialNumber
+}
+
+// CloseDB closes the database.
+func CloseDB() {
+	db.Close()
+}
+
+func init() {
+	var err error
+	db, err = gorm.Open("sqlite3", DefaultDBPath)
+	if err != nil {
+		logrus.Fatalf("couldn't open sqlite database %s: %v", DefaultDBPath, err)
+	}
+
+	db.AutoMigrate(&User{})
+	db.AutoMigrate(&Network{})
+	db.AutoMigrate(&Server{})
+}

+ 205 - 0
pki.go

@@ -0,0 +1,205 @@
+package ovpm
+
+import (
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"encoding/asn1"
+	//"github.com/Sirupsen/logrus"
+	"bytes"
+	"encoding/pem"
+	"fmt"
+	"math/big"
+	"time"
+)
+
+// CA represents x509 Certificate Authority.
+type CA struct {
+	Cert string
+	Key  string // Private Key
+	CSR  string
+}
+
+// Cert represents any certificate - key pair.
+type Cert struct {
+	Cert string
+	Key  string // Private Key
+}
+
+// CreateCA generates a certificate and a key-pair for the CA and returns them.
+func CreateCA() (*CA, error) {
+	key, err := rsa.GenerateKey(rand.Reader, CrtKeyLength)
+	if err != nil {
+		return nil, fmt.Errorf("private key cannot be created: %s", err)
+	}
+
+	val, err := asn1.Marshal(basicConstraints{true, 0})
+	if err != nil {
+		return nil, fmt.Errorf("can not marshal basic constraints: %s", err)
+	}
+
+	names := pkix.Name{CommonName: "CA"}
+	var csrTemplate = x509.CertificateRequest{
+		Subject:            names,
+		SignatureAlgorithm: x509.SHA512WithRSA,
+		ExtraExtensions: []pkix.Extension{
+			{
+				Id:       asn1.ObjectIdentifier{2, 5, 29, 19},
+				Value:    val,
+				Critical: true,
+			},
+		},
+	}
+
+	csrCertificate, err := x509.CreateCertificateRequest(rand.Reader, &csrTemplate, key)
+	if err != nil {
+		return nil, fmt.Errorf("can not create certificate request: %s", err)
+	}
+
+	csr := pem.EncodeToMemory(&pem.Block{
+		Type: "CERTIFICATE REQUEST", Bytes: csrCertificate,
+	})
+
+	// Serial number
+	serial, err := rand.Int(rand.Reader, (&big.Int{}).Exp(big.NewInt(2), big.NewInt(159), nil))
+	if err != nil {
+		return nil, err
+	}
+
+	now := time.Now()
+	// Create the request template
+	template := x509.Certificate{
+		SerialNumber:          serial,
+		Subject:               names,
+		NotBefore:             now.Add(-10 * time.Minute).UTC(),
+		NotAfter:              now.Add(time.Duration(24*365) * time.Hour).UTC(),
+		BasicConstraintsValid: true,
+		IsCA:     true,
+		KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
+		//ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
+	}
+
+	// Sign the certificate authority
+	certificate, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
+	if err != nil {
+		return nil, fmt.Errorf("failed to generate certificate error: %s", err)
+	}
+
+	var request bytes.Buffer
+	var privateKey bytes.Buffer
+	if err := pem.Encode(&request, &pem.Block{Type: "CERTIFICATE", Bytes: certificate}); err != nil {
+		return nil, err
+	}
+	if err := pem.Encode(&privateKey, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}); err != nil {
+		return nil, err
+	}
+
+	return &CA{
+		Key:  privateKey.String(),
+		Cert: request.String(),
+		CSR:  string(csr),
+	}, nil
+
+}
+
+// CreateServerCert generates a x509 certificate and a key-pair for the server.
+func CreateServerCert(ca *CA) (*Cert, error) {
+	return createCert("localhost", ca, true)
+}
+
+// CreateClientCert generates a x509 certificate and a key-pair for the client.
+func CreateClientCert(username string, ca *CA) (*Cert, error) {
+	return createCert(username, ca, false)
+}
+
+func getCertFromPEM(pemCert string) (*x509.Certificate, error) {
+	block, _ := pem.Decode([]byte(pemCert))
+	var cert *x509.Certificate
+	cert, _ = x509.ParseCertificate(block.Bytes)
+	return cert, nil
+}
+
+func createCert(commonName string, ca *CA, server bool) (*Cert, error) {
+	// Get CA private key
+	block, _ := pem.Decode([]byte(ca.Key))
+	if block == nil {
+		return nil, fmt.Errorf("failed to parse ca private key")
+	}
+
+	caKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse ca private key: %s", err)
+	}
+
+	caCert, err := getCertFromPEM(ca.Cert)
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse ca cert: %v", err)
+	}
+
+	// Create new cert's key
+	key, err := rsa.GenerateKey(rand.Reader, 2048)
+	if err != nil {
+		return nil, fmt.Errorf("private key cannot be created: %s", err)
+	}
+
+	serial, err := rand.Int(rand.Reader, (&big.Int{}).Exp(big.NewInt(2), big.NewInt(159), nil))
+	if err != nil {
+		return nil, err
+	}
+
+	tml := x509.Certificate{
+		NotBefore:    time.Now(),
+		NotAfter:     time.Now().AddDate(5, 0, 0),
+		SerialNumber: serial,
+		Subject: pkix.Name{
+			CommonName:   commonName,
+			Organization: []string{"Innovation"},
+		},
+		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
+		BasicConstraintsValid: true,
+	}
+
+	if server {
+		tml.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}
+	}
+
+	// Sign with CA's private key
+	cert, err := x509.CreateCertificate(rand.Reader, &tml, caCert, &key.PublicKey, caKey)
+	if err != nil {
+		return nil, fmt.Errorf("certificate cannot be created: %s", err)
+	}
+
+	priKeyPem := pem.EncodeToMemory(&pem.Block{
+		Type:  "RSA PRIVATE KEY",
+		Bytes: x509.MarshalPKCS1PrivateKey(key),
+	})
+
+	certPem := pem.EncodeToMemory(&pem.Block{
+		Type:  "CERTIFICATE",
+		Bytes: cert,
+	})
+
+	return &Cert{
+		Key:  string(priKeyPem[:]),
+		Cert: string(certPem[:]),
+	}, nil
+}
+
+type basicConstraints struct {
+	IsCA       bool `asn1:"optional"`
+	MaxPathLen int  `asn1:"optional,default:-1"`
+}
+
+func getCA() (*CA, error) {
+	server := Server{}
+	db.First(&server)
+	if db.NewRecord(&server) {
+		return nil, fmt.Errorf("can not retrieve server from db")
+	}
+	return &CA{
+		Cert: server.CACert,
+		Key:  server.CAKey,
+	}, nil
+
+}

+ 2 - 0
template/ccd.file.tmpl

@@ -0,0 +1,2 @@
+ifconfig-push {{ .IP }} {{ .NetMask }}
+#iroute 192.168.90.0 255.255.255.0

+ 21 - 0
template/client.ovpn.tmpl

@@ -0,0 +1,21 @@
+# this ovpn file is automatically generated by [OVPM](https://github.com/cad/ovpm)
+
+client
+dev tun
+proto udp
+remote {{ .Hostname }} {{ .Port }}
+resolv-retry infinite
+nobind
+persist-key
+persist-tun
+comp-lzo
+verb 3
+
+<ca>
+{{ .CA }}</ca>
+
+<cert>
+{{ .Cert }}</cert>
+
+<key>
+{{ .Key }}</key>

+ 24 - 0
template/dh4096.pem.tmpl

@@ -0,0 +1,24 @@
+-----BEGIN DH PARAMETERS-----
+MIIEDQKCAgEAkeaoNKpzWIU4+8hJUfFeHJJki7lzxK9lVJzoT1O3vRD1pv1M0jm4
+d372rq7crH9lolgi0VydQagf+831JZJ7m9/PYfYRnU2bf53j4I8O8CgFBCjWLBgc
+dzPG7I4NGAc1YHV3R+Guh8bFgOvcfiWw4nEMNjkquHcZ7vivEfy5h2OGmbtqkrkQ
+6IV1uFGagwvQzIta3sxHL2r8Xxq3iGoXnW6azGcc6Zv+0p0gZWSkobu2GUdaIsH6
+jum//TZvouwkDEh60utF5wPwoqMye9ahUOexz+7tI7ue1/lre+hLaUWQigzEe56b
+fR5WUZRhwNg9zTngpFTLXiAI39J8xHbUid3kv/CSDPCq6XhUMWyCyLBsSSdc1Ijq
+pR675BBqLwSo4oON/o1TgMYD3/3xSakXSULvSODYlLdnHniL8Kh+B+2egAZeayUb
+pqV6lFUXFo1XjklWQscq2gwuKvw7XCd0PnYad0rxLVb/aLyYKzv4crL5RRs/xOcf
+O2D3/o4ld3nWQ5iB5iH47CGAlnaR/0jD5gGnOF8mgNGx64PMB+W5QekzZ/aams9K
+M0ctXH7vVdQvc+r86KDt/IQXopuooX1K4kmhqUjkWJrjt0UvVRp2skIYSNEeljA0
+NLeC/k0YB0rNWZx3URg3XS/v0oJO+Pt/O5U2iuopHZp2uWh9LeQ+qisCggIAJtmv
+4H727O/aDcty1c8K32pKGym+LGBxx9nuuMTjpa5N/Ww/wtHcQWKVo1HdknGoBOXc
+9a6I7574fJFuwEzQe4b44VYk+t+9pLxsBT/r36Qi1eFyrYgMhoiLgjfgJ1tarXE9
+HJ6jLJm/DIKoxwu5XRHUw1YJ7KDjqw1IZGeMEWhUxsyvPyGviyCiHPbmrvNJzJnP
+7ig1nMbbHi/Al+P9K1forRA5+VOf5fvTPtq6mxoQ585cCrLY3EqQe+fPlUzcBL/b
+fMVnw90b7sQVI9EEXxpp52uIY9xtTzsfaK3Gv3pmTeFEecyda2/Ow8oGYzobn5SA
+O9xEpc2mNUlONT4thBPh/3NYPGZnIiDYN8vHjKiSnYcYRsbWpxvTEZRNEkI6h5dV
+83EvsNMOoG8duJQhluQxka0OhFg0UTGhec11Xs0iTti8CvnOpKqyg2vXlOn0I94P
+IVcAwG0ppLn8Py3sBrYO5HF5pStopEPTceylCSkkOT31lVKEAu4TMBrEZh9x2nIV
+AHGxQVTkeqx8u7k9Hg02tX9Hm6w8aFF7WCPLJJLe+MWHlOtp4C6ep/PFnc6DESo9
+gWCAPLIM7tcb9iSSWBmugSkqVolAgLbX/T95Sxnvn4cwchgtgsoOV2lNUt89B8kf
+YtMGCwfzKB0Ws3SW31ayu9jzDgyIr+isjqk1a7MCAgEA
+-----END DH PARAMETERS-----

+ 0 - 0
template/iptables.tmpl


+ 287 - 0
template/server.conf.tmpl

@@ -0,0 +1,287 @@
+;port 1194
+port {{ .Port }}
+
+# TCP or UDP server?
+;proto tcp
+proto udp
+
+# "dev tun" will create a routed IP tunnel,
+# "dev tap" will create an ethernet tunnel.
+# Use "dev tap0" if you are ethernet bridging
+# and have precreated a tap0 virtual interface
+# and bridged it with your ethernet interface.
+# If you want to control access policies
+# over the VPN, you must create firewall
+# rules for the the TUN/TAP interface.
+# On non-Windows systems, you can give
+# an explicit unit number, such as tun0.
+# On Windows, use "dev-node" for this.
+# On most systems, the VPN will not function
+# unless you partially or fully disable
+# the firewall for the TUN/TAP interface.
+;dev tap
+dev tun
+
+# Windows needs the TAP-Win32 adapter name
+# from the Network Connections panel if you
+# have more than one.  On XP SP2 or higher,
+# you may need to selectively disable the
+# Windows firewall for the TAP adapter.
+# Non-Windows systems usually don't need this.
+;dev-node MyTap
+
+# SSL/TLS root certificate (ca), certificate
+# (cert), and private key (key).  Each client
+# and the server must have their own cert and
+# key file.  The server and all clients will
+# use the same ca file.
+#
+# See the "easy-rsa" directory for a series
+# of scripts for generating RSA certificates
+# and private keys.  Remember to use
+# a unique Common Name for the server
+# and each of the client certificates.
+#
+# Any X509 key management system can be used.
+# OpenVPN can also use a PKCS #12 formatted key file
+# (see "pkcs12" directive in man page).
+;ca easy-rsa/keys/ca.crt
+;cert easy-rsa/keys/server.crt
+;key easy-rsa/keys/server.key  # This file should be kept secret
+
+ca {{ .CACertPath }}
+cert {{ .CertPath }}
+key {{ .KeyPath }}
+
+# Diffie hellman parameters.
+# Generate your own with:
+#   openssl dhparam -out dh1024.pem 1024
+# Substitute 2048 for 1024 if you are using
+# 2048 bit keys.
+#dh dh1024.pem
+;dh easy-rsa/keys/dh2048.pem
+dh {{ .DHParamsPath }}
+
+# Network topology
+# Should be subnet (addressing via IP)
+# unless Windows clients v2.0.9 and lower have to
+# be supported (then net30, i.e. a /30 per client)
+# Defaults to net30 (not recommended)
+topology subnet
+
+# Configure server mode and supply a VPN subnet
+# for OpenVPN to draw client addresses from.
+# The server will take 10.8.0.1 for itself,
+# the rest will be made available to clients.
+# Each client will be able to reach the server
+# on 10.8.0.1. Comment this line out if you are
+# ethernet bridging. See the man page for more info.
+;server 10.8.0.0 255.255.255.0
+server {{ .Net }} {{ .Mask }}
+
+# Maintain a record of client <-> virtual IP address
+# associations in this file.  If OpenVPN goes down or
+# is restarted, reconnecting clients can be assigned
+# the same virtual IP address from the pool that was
+# previously assigned.
+ifconfig-pool-persist ipp.txt
+
+# Configure server mode for ethernet bridging.
+# You must first use your OS's bridging capability
+# to bridge the TAP interface with the ethernet
+# NIC interface.  Then you must manually set the
+# IP/netmask on the bridge interface, here we
+# assume 10.8.0.4/255.255.255.0.  Finally we
+# must set aside an IP range in this subnet
+# (start=10.8.0.50 end=10.8.0.100) to allocate
+# to connecting clients.  Leave this line commented
+# out unless you are ethernet bridging.
+;server-bridge 10.8.0.4 255.255.255.0 10.8.0.50 10.8.0.100
+
+# Configure server mode for ethernet bridging
+# using a DHCP-proxy, where clients talk
+# to the OpenVPN server-side DHCP server
+# to receive their IP address allocation
+# and DNS server addresses.  You must first use
+# your OS's bridging capability to bridge the TAP
+# interface with the ethernet NIC interface.
+# Note: this mode only works on clients (such as
+# Windows), where the client-side TAP adapter is
+# bound to a DHCP client.
+;server-bridge
+
+# Push routes to the client to allow it
+# to reach other private subnets behind
+# the server.  Remember that these
+# private subnets will also need
+# to know to route the OpenVPN client
+# address pool (10.8.0.0/255.255.255.0)
+# back to the OpenVPN server.
+#route 192.168.90.0 255.255.255.0 10.8.0.2
+#route 192.168.91.0 255.255.255.0 10.8.0.5
+#route 192.168.92.0 255.255.255.0 10.8.0.3
+#route 192.168.93.0 255.255.255.0 10.8.0.4
+#push "route 172.16.100.0 255.255.255.0"
+#push "route 192.168.60.0 255.255.255.0"
+# To assign specific IP addresses to specific
+# clients or if a connecting client has a private
+# subnet behind it that should also have VPN access,
+# use the subdirectory "ccd" for client-specific
+# configuration files (see man page for more info).
+
+# EXAMPLE: Suppose the client
+# having the certificate common name "Thelonious"
+# also has a small subnet behind his connecting
+# machine, such as 192.168.40.128/255.255.255.248.
+# First, uncomment out these lines:
+#client-config-dir ccd
+#route 192.168.90.0 255.255.255.0
+# Then create a file ccd/Thelonious with this line:
+#   iroute 192.168.40.128 255.255.255.248
+# This will allow Thelonious' private subnet to
+# access the VPN.  This example will only work
+# if you are routing, not bridging, i.e. you are
+# using "dev tun" and "server" directives.
+
+# EXAMPLE: Suppose you want to give
+# Thelonious a fixed VPN IP address of 10.9.0.1.
+# First uncomment out these lines:
+;client-config-dir ccd
+client-config-dir {{ .CCDPath }}
+# Then add this line to ccd/Thelonious:
+#   ifconfig-push 10.9.0.1 10.9.0.2
+
+# Suppose that you want to enable different
+# firewall access policies for different groups
+# of clients.  There are two methods:
+# (1) Run multiple OpenVPN daemons, one for each
+#     group, and firewall the TUN/TAP interface
+#     for each group/daemon appropriately.
+# (2) (Advanced) Create a script to dynamically
+#     modify the firewall in response to access
+#     from different clients.  See man
+#     page for more info on learn-address script.
+;learn-address ./script
+
+# If enabled, this directive will configure
+# all clients to redirect their default
+# network gateway through the VPN, causing
+# all IP traffic such as web browsing and
+# and DNS lookups to go through the VPN
+# (The OpenVPN server machine may need to NAT
+# or bridge the TUN/TAP interface to the internet
+# in order for this to work properly).
+;push "redirect-gateway def1 bypass-dhcp"
+push "redirect-gateway def1 bypass-dhcp"
+
+# Certain Windows-specific network settings
+# can be pushed to clients, such as DNS
+# or WINS server addresses.  CAVEAT:
+# http://openvpn.net/faq.html#dhcpcaveats
+# The addresses below refer to the public
+# DNS servers provided by opendns.com.
+;push "dhcp-option DNS 208.67.222.222"
+push "dhcp-option DNS 8.8.8.8"
+
+# Uncomment this directive to allow different
+# clients to be able to "see" each other.
+# By default, clients will only see the server.
+# To force clients to only see the server, you
+# will also need to appropriately firewall the
+# server's TUN/TAP interface.
+client-to-client
+
+# Uncomment this directive if multiple clients
+# might connect with the same certificate/key
+# files or common names.  This is recommended
+# only for testing purposes.  For production use,
+# each client should have its own certificate/key
+# pair.
+#
+# IF YOU HAVE NOT GENERATED INDIVIDUAL
+# CERTIFICATE/KEY PAIRS FOR EACH CLIENT,
+# EACH HAVING ITS OWN UNIQUE "COMMON NAME",
+# UNCOMMENT THIS LINE OUT.
+;duplicate-cn
+
+# The keepalive directive causes ping-like
+# messages to be sent back and forth over
+# the link so that each side knows when
+# the other side has gone down.
+# Ping every 10 seconds, assume that remote
+# peer is down if no ping received during
+# a 120 second time period.
+keepalive 10 120
+
+# For extra security beyond that provided
+# by SSL/TLS, create an "HMAC firewall"
+# to help block DoS attacks and UDP port flooding.
+#
+# Generate with:
+#   openvpn --genkey --secret ta.key
+#
+# The server and each client must have
+# a copy of this key.
+# The second parameter should be '0'
+# on the server and '1' on the clients.
+;tls-auth ta.key 0 # This file is secret
+
+# Select a cryptographic cipher.
+# This config item must be copied to
+# the client config file as well.
+;cipher BF-CBC        # Blowfish (default)
+;cipher AES-128-CBC   # AES
+;cipher DES-EDE3-CBC  # Triple-DES
+
+# Enable compression on the VPN link.
+# If you enable it here, you must also
+# enable it in the client config file.
+comp-lzo
+
+# The maximum number of concurrently connected
+# clients we want to allow.
+;max-clients 100
+
+# It's a good idea to reduce the OpenVPN
+# daemon's privileges after initialization.
+#
+# You can uncomment this out on
+# non-Windows systems.
+user nobody
+group nobody
+
+# The persist options will try to avoid
+# accessing certain resources on restart
+# that may no longer be accessible because
+# of the privilege downgrade.
+persist-key
+persist-tun
+
+# Output a short status file showing
+# current connections, truncated
+# and rewritten every minute.
+status openvpn-status.log
+
+# By default, log messages will go to the syslog (or
+# on Windows, if running as a service, they will go to
+# the "\Program Files\OpenVPN\log" directory).
+# Use log or log-append to override this default.
+# "log" will truncate the log file on OpenVPN startup,
+# while "log-append" will append to it.  Use one
+# or the other (but not both).
+;log         openvpn.log
+;log-append  openvpn.log
+
+# Set the appropriate level of log
+# file verbosity.
+#
+# 0 is silent, except for fatal errors
+# 4 is reasonable for general usage
+# 5 and 6 can help to debug connection problems
+# 9 is extremely verbose
+verb 3
+
+# Silence repeating messages.  At most 20
+# sequential messages of the same message
+# category will be output to the log.
+;mute 20

+ 142 - 0
user.go

@@ -0,0 +1,142 @@
+package ovpm
+
+import (
+	"fmt"
+	"github.com/Sirupsen/logrus"
+	"github.com/asaskevich/govalidator"
+)
+
+// GetUser finds and returns the user with the given username from database.
+func GetUser(username string) (*User, error) {
+	user := User{}
+	db.Where(&User{Username: username}).First(&user)
+	if db.NewRecord(&user) {
+		// user is not found
+		return nil, fmt.Errorf("user not found: %s", username)
+	}
+	return &user, nil
+}
+
+// GetAllUsers returns all recorded users in the database.
+func GetAllUsers() ([]*User, error) {
+	var users []*User
+	db.Find(&users)
+
+	return users, nil
+
+}
+
+// CreateUser creates a new user with the given username and password in the database.
+// It also generates the necessary client keys and signs certificates with the current
+// server's CA.
+func CreateUser(username, password string) (*User, error) {
+	if !CheckBootstrapped() {
+		return nil, fmt.Errorf("you first need to create server")
+	}
+	// Validate user input.
+	if govalidator.IsNull(username) {
+		return nil, fmt.Errorf("validation error: %s can not be null", username)
+	}
+	if !govalidator.IsAlphanumeric(username) {
+		return nil, fmt.Errorf("validation error: `%s` can only contain letters and numbers", username)
+	}
+	ca, err := getCA()
+	if err != nil {
+		return nil, err
+	}
+
+	clientCert, err := CreateClientCert(username, ca)
+	if err != nil {
+		return nil, fmt.Errorf("can not create client cert %s: %v", username, err)
+	}
+
+	user := User{
+		Username: username,
+		Password: password,
+		Cert:     clientCert.Cert,
+		Key:      clientCert.Key,
+	}
+
+	db.Create(&user)
+	if db.NewRecord(&user) {
+		// user is still not created
+		return nil, fmt.Errorf("can not create user in database: %s", user.Username)
+	}
+	logrus.Infof("user created: %s", username)
+
+	// Emit server config
+	err = Emit()
+	if err != nil {
+		return nil, err
+	}
+	return &user, nil
+}
+
+// DeleteUser deletes a user by the given username from the database.
+func DeleteUser(username string) error {
+	user := User{}
+	db.Unscoped().Where(&User{Username: username}).First(&user)
+	if db.NewRecord(&user) {
+		// user is not found
+		return fmt.Errorf("user not found: %s", username)
+	}
+	db.Unscoped().Delete(&user)
+
+	err := Emit()
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+// ResetUserPassword resets the users password into the provided password.
+func ResetUserPassword(username, newPassword string) error {
+	user := User{}
+	db.Where(&User{Username: username}).First(&user)
+	if db.NewRecord(&user) {
+		// user is not found
+		return fmt.Errorf("user not found: %s", username)
+	}
+
+	err := user.setPassword(newPassword)
+	if err != nil {
+		// user password can not be updated
+		return fmt.Errorf("user password can not be updated %s: %v", username, err)
+	}
+	return nil
+}
+
+// SignUser create a key and a ceritificate signed by the current server's CA.
+//
+// This is often used to sign users when the current CA is changed while there are
+// still  existing users in the database.
+func SignUser(username string) error {
+	if !CheckBootstrapped() {
+		return fmt.Errorf("you first need to create server")
+	}
+	user, err := GetUser(username)
+	if err != nil {
+		return fmt.Errorf("user not found %s: %v", username, err)
+	}
+	ca, err := getCA()
+	if err != nil {
+		return err
+	}
+
+	clientCert, err := CreateClientCert(username, ca)
+	if err != nil {
+		return fmt.Errorf("can not create client cert %s: %v", username, err)
+	}
+
+	server, err := GetServerInstance()
+	if err != nil {
+		return err
+	}
+
+	user.Cert = clientCert.Cert
+	user.Key = clientCert.Key
+	user.ServerSerialNumber = server.SerialNumber
+
+	db.Save(&user)
+	return nil
+}

+ 409 - 0
vpn.go

@@ -0,0 +1,409 @@
+//go:generate go-bindata -pkg ovpm template/
+
+package ovpm
+
+import (
+	"bytes"
+	"fmt"
+	"github.com/Sirupsen/logrus"
+	"github.com/asaskevich/govalidator"
+	"github.com/google/uuid"
+	"net"
+	"os"
+	"os/exec"
+	"strings"
+	"text/template"
+)
+
+type _VPNServerConfig struct {
+	CertPath     string
+	KeyPath      string
+	CACertPath   string
+	CAKeyPath    string
+	CCDPath      string
+	DHParamsPath string
+	Net          string
+	Mask         string
+	Port         string
+}
+
+// CreateServer generates keys and certs for a Root CA, and saves them in the database.
+func CreateServer(serverName string, hostname string, port string) error {
+	if CheckBootstrapped() {
+		return fmt.Errorf("server is already created")
+	}
+	if !govalidator.IsHost(hostname) {
+		return fmt.Errorf("validation error: hostname:`%s` should be either an ip address or a FQDN", hostname)
+	}
+
+	ca, err := CreateCA()
+	if err != nil {
+		return fmt.Errorf("can not create ca creds: %s", err)
+	}
+
+	srv, err := CreateServerCert(ca)
+	if err != nil {
+		return fmt.Errorf("can not create server cert creds: %s", err)
+	}
+	serialNumber := uuid.New().String()
+
+	serverInstance := Server{
+		Name: serverName,
+
+		SerialNumber: serialNumber,
+		Hostname:     hostname,
+		Port:         port,
+		Cert:         srv.Cert,
+		Key:          srv.Key,
+		CACert:       ca.Cert,
+		CAKey:        ca.Key,
+		Net:          DefaultServerNetwork,
+		Mask:         DefaultServerNetMask,
+	}
+
+	db.Create(&serverInstance)
+
+	if db.NewRecord(&serverInstance) {
+		return fmt.Errorf("can not create server instance on database")
+	}
+
+	users, err := GetAllUsers()
+	if err != nil {
+		return err
+	}
+	// Sign all users in the db with the new server
+	for _, user := range users {
+		err := SignUser(user.Username)
+		logrus.Infof("user certificate changed for %s, you should run: $ ovpm user export-config --user %s", user.Username, user.Username)
+		if err != nil {
+			logrus.Errorf("can not sign user %s: %v", user.Username, err)
+			continue
+		}
+	}
+	return nil
+}
+
+// DeleteServer deletes the server with the given serverName from the database.
+func DeleteServer(serverName string) error {
+	if !CheckBootstrapped() {
+		return fmt.Errorf("server not found")
+	}
+
+	db.Unscoped().Delete(&Server{})
+	return nil
+}
+
+// DumpUserOVPNConf combines a specially generated config for the client with CA's and Client's certs and Clients key then dumps them to the specified path.
+func DumpUserOVPNConf(username, outPath string) error {
+	var result bytes.Buffer
+	user, err := GetUser(username)
+	if err != nil {
+		return err
+	}
+
+	server, err := GetServerInstance()
+	if err != nil {
+		return err
+	}
+
+	params := struct {
+		Hostname string
+		Port     string
+		CA       string
+		Key      string
+		Cert     string
+	}{
+		Hostname: server.Hostname,
+		Port:     server.Port,
+		CA:       server.CACert,
+		Key:      user.Key,
+		Cert:     user.Cert,
+	}
+	data, err := Asset("template/client.ovpn.tmpl")
+	if err != nil {
+		return err
+	}
+
+	t, err := template.New("client.ovpn").Parse(string(data))
+	if err != nil {
+		return fmt.Errorf("can not parse client.ovpn.tmpl template: %s", err)
+	}
+
+	err = t.Execute(&result, params)
+	if err != nil {
+		return fmt.Errorf("can not render client.ovpn: %s", err)
+	}
+
+	// Wite rendered content into openvpn server conf.
+	return emitToFile(outPath, result.String(), 0)
+
+}
+
+// Emit generates all needed files for the OpenVPN server and dumps them to their corresponding paths defined in the config.
+func Emit() error {
+	// Check dependencies
+	if !checkOpenVPNBinary() {
+		return fmt.Errorf("openvpn binary can not be found! you should install OpenVPN on this machine")
+	}
+
+	if !checkOpenSSLBinary() {
+		return fmt.Errorf("openssl binary can not be found! you should install openssl on this machine")
+
+	}
+
+	if !checkIptablesBinary() {
+		return fmt.Errorf("iptables binary can not be found")
+	}
+
+	if !CheckBootstrapped() {
+		return fmt.Errorf("you should create a server first. e.g. $ ovpm vpn create-server")
+	}
+
+	if err := emitServerConf(); err != nil {
+		return fmt.Errorf("can not emit server conf: %s", err)
+	}
+
+	if err := emitServerCert(); err != nil {
+		return fmt.Errorf("can not emit server cert: %s", err)
+	}
+
+	if err := emitServerKey(); err != nil {
+		return fmt.Errorf("can not emit server key: %s", err)
+	}
+
+	if err := emitCACert(); err != nil {
+		return fmt.Errorf("can not emit ca cert : %s", err)
+	}
+
+	if err := emitCAKey(); err != nil {
+		return fmt.Errorf("can not emit ca key: %s", err)
+	}
+
+	if err := emitDHParams(); err != nil {
+		return fmt.Errorf("can not emit dhparams: %s", err)
+	}
+
+	if err := emitCCD(); err != nil {
+		return fmt.Errorf("can not emit ccd: %s", err)
+	}
+
+	if err := emitIptables(); err != nil {
+		return fmt.Errorf("can not emit iptables conf: %s", err)
+	}
+
+	logrus.Info("changes are applied to the filesystem")
+
+	return nil
+}
+
+func emitToFile(filePath, content string, mode uint) error {
+	file, err := os.Create(filePath)
+	if err != nil {
+		return fmt.Errorf("Cannot create file %s: %v", filePath, err)
+
+	}
+	if mode != 0 {
+		file.Chmod(os.FileMode(mode))
+	}
+	defer file.Close()
+	fmt.Fprintf(file, content)
+	return nil
+}
+
+func emitServerConf() error {
+	var result bytes.Buffer
+
+	server := _VPNServerConfig{
+		CertPath:     DefaultCertPath,
+		KeyPath:      DefaultKeyPath,
+		CACertPath:   DefaultCACertPath,
+		CAKeyPath:    DefaultCAKeyPath,
+		CCDPath:      DefaultVPNCCDPath,
+		DHParamsPath: DefaultDHParamsPath,
+		Net:          DefaultServerNetwork,
+		Mask:         DefaultServerNetMask,
+		Port:         DefaultVPNPort,
+	}
+	data, err := Asset("template/server.conf.tmpl")
+	if err != nil {
+		return err
+	}
+
+	t, err := template.New("server.conf").Parse(string(data))
+	if err != nil {
+		return fmt.Errorf("can not parse server.conf.tmpl template: %s", err)
+	}
+
+	err = t.Execute(&result, server)
+	if err != nil {
+		return fmt.Errorf("can not render server.conf: %s", err)
+	}
+
+	// Wite rendered content into openvpn server conf.
+	return emitToFile(DefaultVPNConfPath, result.String(), 0)
+}
+
+// GetServerInstance returns the default server from the database.
+func GetServerInstance() (*Server, error) {
+	var server Server
+	db.First(&server)
+	if db.NewRecord(server) {
+		return nil, fmt.Errorf("can not retrieve server from db")
+	}
+	return &server, nil
+}
+
+// CheckBootstrapped checks if there is a default server in the database or not.
+func CheckBootstrapped() bool {
+	var server Server
+	db.First(&server)
+	if db.NewRecord(server) {
+		return false
+	}
+	return true
+}
+
+func emitServerKey() error {
+	server, err := GetServerInstance()
+	if err != nil {
+		return err
+	}
+
+	// Write rendered content into key file.
+	return emitToFile(DefaultKeyPath, server.Key, 0600)
+}
+
+func emitServerCert() error {
+	server, err := GetServerInstance()
+	if err != nil {
+		return err
+	}
+
+	// Write rendered content into the cert file.
+	return emitToFile(DefaultCertPath, server.Cert, 0)
+}
+
+func emitCACert() error {
+	server, err := GetServerInstance()
+	if err != nil {
+		return err
+	}
+
+	// Write rendered content into the ca cert file.
+	return emitToFile(DefaultCACertPath, server.CACert, 0)
+}
+
+func emitCAKey() error {
+	server, err := GetServerInstance()
+	if err != nil {
+		return err
+	}
+
+	// Write rendered content into the ca key file.
+	return emitToFile(DefaultCAKeyPath, server.CAKey, 0600)
+}
+
+func emitCCD() error {
+	users, err := GetAllUsers()
+	if err != nil {
+		return err
+	}
+
+	// Create and write rendered ccd data.
+	os.Mkdir(DefaultVPNCCDPath, 0755)
+	clientsNetMask := net.IPMask(net.ParseIP(DefaultServerNetMask))
+	clientsNetPrefix := net.ParseIP(DefaultServerNetwork)
+	clientNet := clientsNetPrefix.Mask(clientsNetMask).To4()
+
+	counter := 2
+	for _, user := range users {
+		var result bytes.Buffer
+		clientNet[3] = byte(counter)
+		params := struct {
+			IP      string
+			NetMask string
+		}{IP: clientNet.String(), NetMask: DefaultServerNetMask}
+
+		data, err := Asset("template/ccd.file.tmpl")
+		if err != nil {
+			return err
+		}
+		t, err := template.New("ccd.file.tmpl").Parse(string(data))
+		if err != nil {
+			return fmt.Errorf("can not parse ccd.file.tmpl template: %s", err)
+		}
+
+		err = t.Execute(&result, params)
+		if err != nil {
+			return fmt.Errorf("can not render ccd file %s: %s", user.Username, err)
+		}
+
+		err = emitToFile(DefaultVPNCCDPath+user.Username, result.String(), 0)
+		if err != nil {
+			return err
+		}
+		counter++
+	}
+	return nil
+}
+
+func emitDHParams() error {
+	var result bytes.Buffer
+	data, err := Asset("template/dh4096.pem.tmpl")
+	if err != nil {
+		return err
+	}
+
+	t, err := template.New("dh4096.pem.tmpl").Parse(string(data))
+	if err != nil {
+		return fmt.Errorf("can not parse dh4096.pem template: %s", err)
+	}
+
+	err = t.Execute(&result, nil)
+	if err != nil {
+		return fmt.Errorf("can not render dh4096.pem file: %s", err)
+	}
+
+	err = emitToFile(DefaultDHParamsPath, result.String(), 0)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func emitIptables() error {
+	return nil
+}
+
+func checkOpenVPNBinary() bool {
+	cmd := exec.Command("which", "openvpn")
+	output, err := cmd.Output()
+	if err != nil {
+		logrus.Errorf("openvpn is not installed: %s  ✘", err)
+		return false
+	}
+	logrus.Infof("openvpn binary detected: %s  ✔", strings.TrimSpace(string(output[:])))
+	return true
+}
+
+func checkOpenSSLBinary() bool {
+	cmd := exec.Command("which", "openssl")
+	output, err := cmd.Output()
+	if err != nil {
+		logrus.Errorf("openssl is not installed: %s  ✘", err)
+		return false
+	}
+	logrus.Infof("openssl binary detected: %s  ✔", strings.TrimSpace(string(output[:])))
+	return true
+}
+
+func checkIptablesBinary() bool {
+	cmd := exec.Command("which", "iptables")
+	output, err := cmd.Output()
+	if err != nil {
+		logrus.Errorf("iptables is not installed: %s  ✘", err)
+		return false
+	}
+	logrus.Infof("iptables binary detected: %s  ✔", strings.TrimSpace(string(output[:])))
+	return true
+}