Sfoglia il codice sorgente

Merge branch 'dev'

Mustafa Arici 8 anni fa
parent
commit
3aa29e20fb
21 ha cambiato i file con 950 aggiunte e 229 eliminazioni
  1. 30 3
      .travis.yml
  2. 4 0
      Makefile
  3. 1 0
      README.md
  4. 40 12
      api/rpc.go
  5. 3 3
      bindata/bindata.go
  6. 32 0
      build.sh
  7. 128 24
      cmd/ovpm/main.go
  8. 0 16
      cmd/ovpmd/main.go
  9. 1 0
      contrib/afterinstall.sh
  10. 18 0
      contrib/systemd/ovpmd.service
  11. 36 2
      net.go
  12. 177 31
      pb/user.pb.go
  13. 19 0
      pb/user.proto
  14. 21 74
      pb/vpn.pb.go
  15. 0 3
      pb/vpn.proto
  16. 19 2
      pki/pki.go
  17. 5 0
      template/client.ovpn.tmpl
  18. 189 13
      user.go
  19. 97 10
      user_test.go
  20. 40 16
      vpn.go
  21. 90 20
      vpn_test.go

+ 30 - 3
.travis.yml

@@ -1,16 +1,43 @@
+sudo: required
+services:
+  - docker
+
+cache:
+    directories:
+     - $HOME/.cache
+
 language: go
 go:
   - 1.8
   - tip
-
+go_import_path: github.com/cad/ovpm
+env:
+    matrix:
+      - OS=el DIST=7
+      - OS=fedora DIST=25
 
 before_install:
   - sudo apt-get -qq update
   - sudo apt-get install -y openvpn
   - go get -t -v ./...
 
+after_success:
+  - bash <(curl -s https://codecov.io/bash)
+
+
 script:
   - go test -race -coverprofile=coverage.txt -covermode=atomic .
+  - make docker-build
 
-after_success:
-- bash <(curl -s https://codecov.io/bash)
+deploy:
+  # Deploy packages to PackageCloud
+  provider: packagecloud
+  username: ${PACKAGECLOUD_USER}
+  repository: ${PACKAGECLOUD_REPO}
+  token: ${PACKAGECLOUD_TOKEN}
+  dist: ${OS}/${DIST}
+  package_glob: rpm/*.{deb,rpm}
+  skip_cleanup: true
+  on:
+    tag: true
+    #branch: master

+ 4 - 0
Makefile

@@ -0,0 +1,4 @@
+.PHONY: build
+docker-build:
+	docker run --rm -i -t -v `pwd`:/fs/src/github.com/cad/ovpm -w /fs/src/github.com/cad/ovpm fedora ./build.sh
+	#docker run --rm -i -t -v `pwd`:/fs/src/github.com/cad/ovpm -w /fs/src/github.com/cad/ovpm fedora /bin/bash

+ 1 - 0
README.md

@@ -1,6 +1,7 @@
 # OVPM - OpenVPn Manager
 
 [![Build Status](https://travis-ci.org/cad/ovpm.svg?branch=master)](https://travis-ci.org/cad/ovpm)
+[![GitHub version](https://badge.fury.io/gh/cad%2Fovpm.svg)](https://badge.fury.io/gh/cad%2Fovpm)
 [![codecov](https://codecov.io/gh/cad/ovpm/branch/master/graph/badge.svg)](https://codecov.io/gh/cad/ovpm)
 [![GoDoc](https://godoc.org/github.com/cad/ovpm?status.svg)](https://godoc.org/github.com/cad/ovpm)
 

+ 40 - 12
api/rpc.go

@@ -1,5 +1,3 @@
-//go:generate protoc -I pb/ pb/user.proto pb/vpn.proto --go_out=plugins=grpc:pb
-
 package api
 
 import (
@@ -29,6 +27,9 @@ func (s *UserService) List(ctx context.Context, req *pb.UserListRequest) (*pb.Us
 			ServerSerialNumber: user.GetServerSerialNumber(),
 			Username:           user.GetUsername(),
 			CreatedAt:          user.GetCreatedAt(),
+			IPNet:              user.GetIPNet(),
+			NoGW:               user.IsNoGW(),
+			HostID:             user.GetHostID(),
 		})
 	}
 
@@ -38,15 +39,49 @@ func (s *UserService) List(ctx context.Context, req *pb.UserListRequest) (*pb.Us
 func (s *UserService) Create(ctx context.Context, req *pb.UserCreateRequest) (*pb.UserResponse, error) {
 	logrus.Debugf("rpc call: user create: %s", req.Username)
 	var ut []*pb.UserResponse_User
-	user, err := ovpm.CreateNewUser(req.Username, req.Password)
+	user, err := ovpm.CreateNewUser(req.Username, req.Password, req.NoGW, req.HostID)
+	if err != nil {
+		return nil, err
+	}
+
+	pbUser := pb.UserResponse_User{
+		Username:           user.GetUsername(),
+		ServerSerialNumber: user.GetServerSerialNumber(),
+		NoGW:               user.IsNoGW(),
+		HostID:             user.GetHostID(),
+	}
+	ut = append(ut, &pbUser)
+
+	return &pb.UserResponse{Users: ut}, nil
+}
+
+func (s *UserService) Update(ctx context.Context, req *pb.UserUpdateRequest) (*pb.UserResponse, error) {
+	logrus.Debugf("rpc call: user update: %s", req.Username)
+	var ut []*pb.UserResponse_User
+	user, err := ovpm.GetUser(req.Username)
 	if err != nil {
 		return nil, err
 	}
+	var noGW bool
+
+	switch req.Gwpref {
+	case pb.UserUpdateRequest_NOGW:
+		noGW = false
+	case pb.UserUpdateRequest_GW:
+		noGW = true
+	default:
+		noGW = user.NoGW
+
+	}
 
+	user.Update(req.Password, noGW, req.HostID)
 	pbUser := pb.UserResponse_User{
 		Username:           user.GetUsername(),
 		ServerSerialNumber: user.GetServerSerialNumber(),
+		NoGW:               user.IsNoGW(),
+		HostID:             user.GetHostID(),
 	}
+
 	ut = append(ut, &pbUser)
 
 	return &pb.UserResponse{Users: ut}, nil
@@ -63,6 +98,7 @@ func (s *UserService) Delete(ctx context.Context, req *pb.UserDeleteRequest) (*p
 	pbUser := pb.UserResponse_User{
 		Username:           user.GetUsername(),
 		ServerSerialNumber: user.GetServerSerialNumber(),
+		HostID:             user.GetHostID(),
 	}
 	ut = append(ut, &pbUser)
 
@@ -85,6 +121,7 @@ func (s *UserService) Renew(ctx context.Context, req *pb.UserRenewRequest) (*pb.
 	pbUser := pb.UserResponse_User{
 		Username:           user.GetUsername(),
 		ServerSerialNumber: user.GetServerSerialNumber(),
+		HostID:             user.GetHostID(),
 	}
 	ut = append(ut, &pbUser)
 
@@ -140,12 +177,3 @@ func (s *VPNService) Init(ctx context.Context, req *pb.VPNInitRequest) (*pb.VPNI
 	}
 	return &pb.VPNInitResponse{}, nil
 }
-
-func (s *VPNService) Apply(ctx context.Context, req *pb.VPNApplyRequest) (*pb.VPNApplyResponse, error) {
-	logrus.Debugf("rpc call: vpn apply")
-	if err := ovpm.Emit(); err != nil {
-		logrus.Errorf("can not apply configuration: %v", err)
-		return nil, err
-	}
-	return &pb.VPNApplyResponse{}, nil
-}

+ 3 - 3
bindata/bindata.go

@@ -87,12 +87,12 @@ func templateCcdFileTmpl() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "template/ccd.file.tmpl", size: 74, mode: os.FileMode(420), modTime: time.Unix(1502796579, 0)}
+	info := bindataFileInfo{name: "template/ccd.file.tmpl", size: 74, mode: os.FileMode(420), modTime: time.Unix(1502998813, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
 
-var _templateClientOvpnTmpl = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x3c\x8e\xb1\x6a\x03\x31\x0c\x86\x77\x3d\x85\xa0\x4b\x3b\x5c\x3c\x74\x2b\x47\xa1\x74\x29\x94\xd2\x4c\x5d\x4a\x07\x9f\x4f\x49\x4c\x6c\xcb\xc8\x3a\x83\x1b\xfc\xee\xe5\x2e\xa1\x9b\xfe\x4f\x48\xdf\x7f\x87\x7a\xf2\x05\xb9\xe6\x84\x07\x1f\x08\x7d\x41\xbb\x28\x47\xab\xde\xd9\x10\x1a\x1e\x29\x91\x58\xa5\x19\xa7\x86\xdf\x9f\x5f\xfb\x8f\x9f\xfb\x93\x6a\x2e\x4f\xc6\x1c\xbd\x9e\x96\x69\xe7\x38\x1a\x67\x67\xc3\x35\xc7\x07\x00\x17\x3c\x25\x85\x99\x2a\xea\x92\x20\x0b\x2b\xe3\x32\x67\x10\x8a\xac\x84\x97\x0b\xee\xde\xb8\x68\xb2\x91\xb0\xf7\x2d\xef\x59\x14\x7b\x07\xa1\xc2\xa1\x0e\x42\x2a\x0d\x7d\x3a\xf8\xe4\x95\x6e\x87\x83\x23\xd1\x41\x43\xc1\x42\x52\x49\x20\xf1\xe4\xd3\x0c\x99\xa4\xf8\xa2\xc3\x99\xda\xff\xbc\x8a\x1d\xc7\x3c\x84\x5f\x86\x4a\x32\xe1\x23\xc0\xe8\xec\x33\xac\xb6\xd7\x17\xec\x7d\x34\x6b\x1c\xd7\xa7\x37\x4a\x5b\x87\xd1\x5c\xd1\x78\xa6\x76\x5d\xbc\x53\xdb\xf8\x06\xfe\x02\x00\x00\xff\xff\x96\xf9\x3e\xe7\x32\x01\x00\x00")
+var _templateClientOvpnTmpl = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x3c\xcf\x31\x4b\x04\x31\x10\x05\xe0\x3e\xbf\x62\xc0\x46\x8b\xbd\x2d\xec\x64\x11\xc4\x42\x41\xd4\xab\xb4\x10\x8b\x6c\xf6\xdd\x5d\xb8\x6c\x26\x4c\x66\x17\xe2\xb1\xff\x5d\xb2\x77\x5c\x97\xf7\x3d\x12\x5e\x6e\x48\x0f\x3e\x13\xcf\x29\xd2\xce\x07\x90\xcf\x64\x27\xe5\xd1\xaa\x77\x36\x84\x42\x7b\x44\x88\x55\x0c\xd4\x17\xfa\xf9\xfc\xda\xbe\xff\xde\x1e\x54\x53\x7e\x68\xdb\xbd\xd7\xc3\xd4\x6f\x1c\x8f\xad\xb3\x43\xcb\x73\x1a\xef\x8c\x71\xc1\x23\xaa\x19\x30\x93\x4e\xd1\x24\x61\x65\x9a\x86\x64\x04\x23\x2b\xe8\x74\xa2\xcd\x2b\x67\x8d\x76\x04\x2d\xcb\x9a\xb7\x2c\x4a\xcb\x62\x04\x99\xc3\xdc\x08\x54\x0a\xf9\xb8\xf3\xd1\x2b\x2e\x17\x1b\x07\xd1\x46\x43\xa6\x0c\x99\x21\x26\xe6\x0b\x95\x84\xab\x71\xef\xe3\x60\x12\x24\xfb\xac\xcd\x11\xe5\x7a\xae\x63\x1c\x8f\xa9\x09\x7f\x6c\x66\x48\x4f\xf7\xc6\x74\xce\x3e\x9a\xba\xe0\xf9\x89\x96\xa5\x6b\x6b\xec\xea\xab\x17\xc5\xba\xab\x6b\xcf\xd4\x1d\x51\xce\xc5\x1b\xca\xea\x2b\x54\xf1\x3b\xda\x7c\xf0\xcb\xf7\xfa\x0b\x9e\x14\x4d\xe4\x34\x85\x50\x3b\xc4\xa1\xf2\x7f\x00\x00\x00\xff\xff\x7e\x3b\xec\xc9\x6d\x01\x00\x00")
 
 func templateClientOvpnTmplBytes() ([]byte, error) {
 	return bindataRead(
@@ -107,7 +107,7 @@ func templateClientOvpnTmpl() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "template/client.ovpn.tmpl", size: 306, mode: os.FileMode(420), modTime: time.Unix(1502796579, 0)}
+	info := bindataFileInfo{name: "template/client.ovpn.tmpl", size: 365, mode: os.FileMode(420), modTime: time.Unix(1503002198, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }

+ 32 - 0
build.sh

@@ -0,0 +1,32 @@
+#!/bin/bash
+set -x
+
+# deps
+rpm --import https://mirror.go-repo.io/fedora/RPM-GPG-KEY-GO-REPO
+curl -s https://mirror.go-repo.io/fedora/go-repo.repo | tee /etc/yum.repos.d/go-repo.repo
+yum -y install golang ruby ruby-devel gcc make redhat-rpm-config git rpm-build rpmdevtools
+gem install fpm
+
+# prep
+export DIR="/fs/src/github.com/cad/ovpm"
+export UNITDIR="/usr/lib/systemd/system/"
+export GOPATH="/fs/"
+#export PATH=":$PATH"
+mkdir -p $DIR/build/
+mkdir -p $DIR/rpm/
+rm -rf $DIR/build/*
+rm -rf $DIR/rpm/*
+mkdir -p $DIR/build/usr/sbin/
+mkdir -p $DIR/build/usr/bin/
+mkdir -p $DIR/build/var/db/ovpm
+mkdir -p $DIR/build/$UNITDIR
+go get -v -t -d ./...
+
+#build
+#install
+GOOS=linux  go build  -o $DIR/build/usr/sbin/ovpmd ./cmd/ovpmd
+GOOS=linux  go build  -o $DIR/build/usr/bin/ovpm   ./cmd/ovpm
+cp $DIR/contrib/systemd/ovpmd.service $DIR/build/$UNITDIR
+
+#package
+fpm -s dir -t rpm -n ovpm --version `git name-rev --tags --name-only $(git rev-parse HEAD) | cut -d 'v' -f 2` --iteration 1 --depends openvpn --description "OVPM makes all aspects of OpenVPN server administration a breeze." --after-install $DIR/contrib/afterinstall.sh -p $DIR/rpm -C $DIR/build .

+ 128 - 24
cmd/ovpm/main.go

@@ -1,10 +1,9 @@
-//go:generate go-bindata template/
-
 package main
 
 import (
 	"context"
 	"fmt"
+	"net"
 	"os"
 
 	"github.com/Sirupsen/logrus"
@@ -68,10 +67,14 @@ func main() {
 							return err
 						}
 						table := tablewriter.NewWriter(os.Stdout)
-						table.SetHeader([]string{"#", "username", "created at", "valid crt"})
+						table.SetHeader([]string{"#", "username", "ip", "created at", "valid crt", "no gw"})
 						//table.SetBorder(false)
 						for i, user := range resp.Users {
-							data := []string{fmt.Sprintf("%v", i+1), user.Username, user.CreatedAt, fmt.Sprintf("%t", user.ServerSerialNumber == server.SerialNumber)}
+							static := ""
+							if user.HostID != 0 {
+								static = "s"
+							}
+							data := []string{fmt.Sprintf("%v", i+1), user.Username, fmt.Sprintf("%s %s", user.IPNet, static), user.CreatedAt, fmt.Sprintf("%t", user.ServerSerialNumber == server.SerialNumber), fmt.Sprintf("%t", user.NoGW)}
 							table.Append(data)
 						}
 						table.Render()
@@ -91,23 +94,46 @@ func main() {
 							Name:  "password, p",
 							Usage: "password for the vpn user",
 						},
+						cli.BoolFlag{
+							Name:  "no-gw",
+							Usage: "don't push vpn server as default gateway for this user",
+						},
+						cli.StringFlag{
+							Name:  "static",
+							Usage: "ip address for the vpn user",
+						},
 					},
 					Action: func(c *cli.Context) error {
 						action = "user:create"
 						username := c.String("username")
 						password := c.String("password")
+						noGW := c.Bool("no-gw")
+						static := c.String("static")
 
 						if username == "" || password == "" {
 							fmt.Println(cli.ShowSubcommandHelp(c))
 							os.Exit(1)
 						}
 
+						var hostid uint32
+						if static != "" {
+							h := ovpm.IP2HostID(net.ParseIP(static).To4())
+							if h == 0 {
+								fmt.Println("--static flag takes a valid ipv4 address")
+								fmt.Println()
+								fmt.Println(cli.ShowSubcommandHelp(c))
+								os.Exit(1)
+							}
+
+							hostid = h
+						}
+
 						//conn := getConn(c.String("port"))
 						conn := getConn(c.GlobalString("daemon-port"))
 						defer conn.Close()
 						userSvc := pb.NewUserServiceClient(conn)
 
-						response, err := userSvc.Create(context.Background(), &pb.UserCreateRequest{Username: username, Password: password})
+						response, err := userSvc.Create(context.Background(), &pb.UserCreateRequest{Username: username, Password: password, NoGW: noGW, HostID: hostid})
 						if err != nil {
 							logrus.Errorf("user can not be created '%s': %v", username, err)
 							os.Exit(1)
@@ -117,6 +143,103 @@ func main() {
 						return nil
 					},
 				},
+				{
+					Name:  "update",
+					Usage: "Update a VPN user.",
+					Flags: []cli.Flag{
+						cli.StringFlag{
+							Name:  "username, u",
+							Usage: "username of the vpn user to update",
+						},
+						cli.StringFlag{
+							Name:  "password, p",
+							Usage: "new password for the vpn user",
+						},
+						cli.BoolFlag{
+							Name:  "no-gw",
+							Usage: "don't push vpn server as default gateway for this user",
+						},
+						cli.BoolFlag{
+							Name:  "gw",
+							Usage: "push vpn server as default gateway for this user",
+						},
+						cli.StringFlag{
+							Name:  "static",
+							Usage: "ip address for the vpn user",
+						},
+					},
+					Action: func(c *cli.Context) error {
+						action = "user:update"
+						username := c.String("username")
+						password := c.String("password")
+						nogw := c.Bool("no-gw")
+						gw := c.Bool("gw")
+						static := c.String("static")
+
+						if username == "" {
+							fmt.Println(cli.ShowSubcommandHelp(c))
+							os.Exit(1)
+						}
+
+						if !(password != "" || gw || nogw) {
+							fmt.Println("nothing is updated!")
+							fmt.Println()
+							fmt.Println(cli.ShowSubcommandHelp(c))
+							os.Exit(1)
+						}
+
+						var hostid uint32
+						if static != "" {
+							h := ovpm.IP2HostID(net.ParseIP(static).To4())
+							if h == 0 {
+								fmt.Println("--static flag takes a valid ipv4 address")
+								fmt.Println()
+								fmt.Println(cli.ShowSubcommandHelp(c))
+								os.Exit(1)
+							}
+
+							hostid = h
+						}
+
+						var gwPref pb.UserUpdateRequest_GWPref
+
+						switch {
+						case gw && !nogw:
+							gwPref = pb.UserUpdateRequest_GW
+						case !gw && nogw:
+							gwPref = pb.UserUpdateRequest_NOGW
+						case gw && nogw:
+							// Ambigius.
+							fmt.Println("you can't use --gw together with --no-gw")
+							fmt.Println()
+							fmt.Println(cli.ShowSubcommandHelp(c))
+							os.Exit(1)
+						default:
+							gwPref = pb.UserUpdateRequest_NOPREF
+
+						}
+
+						//conn := getConn(c.String("port"))
+						conn := getConn(c.GlobalString("daemon-port"))
+						defer conn.Close()
+						userSvc := pb.NewUserServiceClient(conn)
+
+						response, err := userSvc.Update(context.Background(), &pb.UserUpdateRequest{
+							Username: username,
+							Password: password,
+							Gwpref:   gwPref,
+							HostID:   hostid,
+						})
+
+						if err != nil {
+							logrus.Errorf("user can not be updated '%s': %v", username, err)
+							os.Exit(1)
+							return err
+						}
+						logrus.Infof("user updated: %s", response.Users[0].Username)
+						return nil
+					},
+				},
 				{
 					Name:  "delete",
 					Usage: "Delete a VPN user.",
@@ -322,25 +445,6 @@ func main() {
 						return nil
 					},
 				},
-				{
-					Name:  "apply",
-					Usage: "Apply pending changes.",
-					Action: func(c *cli.Context) error {
-						action = "apply"
-
-						conn := getConn(c.GlobalString("daemon-port"))
-						defer conn.Close()
-						vpnSvc := pb.NewVPNServiceClient(conn)
-
-						if _, err := vpnSvc.Apply(context.Background(), &pb.VPNApplyRequest{}); err != nil {
-							logrus.Errorf("can not apply configuration: %v", err)
-							os.Exit(1)
-							return err
-						}
-						logrus.Info("changes applied; OpenVPN restarted")
-						return nil
-					},
-				},
 			},
 		},
 	}

+ 0 - 16
cmd/ovpmd/main.go

@@ -98,22 +98,6 @@ func (s *server) start() {
 	logrus.Infof("OVPM is running :%s ...", s.port)
 	go s.grpcServer.Serve(s.lis)
 	ovpm.StartVPNProc()
-
-	// Nat enablerer
-	go func() {
-		for {
-			err := ovpm.EnsureNatEnabled()
-			if err == nil {
-				logrus.Debug("nat is enabled")
-				return
-			}
-			logrus.Debugf("can not enable nat: %v", err)
-			// TODO(cad): employ a exponential back-off approach here
-			// instead of sleeping for the constant duration.
-			time.Sleep(1 * time.Second)
-		}
-
-	}()
 }
 
 func (s *server) stop() {

+ 1 - 0
contrib/afterinstall.sh

@@ -0,0 +1 @@
+systemctl daemon-reload

+ 18 - 0
contrib/systemd/ovpmd.service

@@ -0,0 +1,18 @@
+[Unit]
+Description=OpenVPn Manager
+Before=multi-user.target
+Before=shutdown.target
+After=local-fs.target
+After=remote-fs.target
+After=network-online.target
+After=systemd-journald-dev-log.socket
+Wants=network-online.target
+Conflicts=shutdown.target
+
+[Service]
+TimeoutSec=5min
+PIDFile=/var/run/ovpmd.pid
+ExecStart=/sbin/ovpmd
+
+[Install]
+WantedBy=multi-user.target

+ 36 - 2
net.go

@@ -1,9 +1,12 @@
 package ovpm
 
 import (
+	"encoding/binary"
 	"fmt"
 	"net"
 
+	"time"
+
 	"github.com/Sirupsen/logrus"
 	"github.com/coreos/go-iptables/iptables"
 )
@@ -110,8 +113,27 @@ func routableIP(network string, ip net.IP) net.IP {
 	return nil
 }
 
-// EnsureNatEnabled is an idempotent command that ensures nat is enabled for the vpn server.
-func EnsureNatEnabled() error {
+// ensureNatEnabled launches a goroutine that constantly tries to enable nat.
+func ensureNatEnabled() {
+	// Nat enablerer
+	go func() {
+		for {
+			err := enableNat()
+			if err == nil {
+				logrus.Debug("nat is enabled")
+				return
+			}
+			logrus.Debugf("can not enable nat: %v", err)
+			// TODO(cad): employ a exponential back-off approach here
+			// instead of sleeping for the constant duration.
+			time.Sleep(1 * time.Second)
+		}
+
+	}()
+}
+
+// enableNat is an idempotent command that ensures nat is enabled for the vpn server.
+func enableNat() error {
 	rif := routedInterface("ip", net.FlagUp|net.FlagBroadcast)
 	if rif == nil {
 		return fmt.Errorf("can not get routable network interface")
@@ -136,4 +158,16 @@ func EnsureNatEnabled() error {
 	ipt.AppendUnique("filter", "FORWARD", "-i", rif.Name, "-o", vpnIfc.Name, "-m", "state", "--state", "RELATED, ESTABLISHED", "-j", "ACCEPT")
 	ipt.AppendUnique("filter", "FORWARD", "-i", vpnIfc.Name, "-o", rif.Name, "-j", "ACCEPT")
 	return nil
+
+}
+
+func HostID2IP(hostid uint32) net.IP {
+	ip := make([]byte, 4)
+	binary.BigEndian.PutUint32(ip, hostid)
+	return net.IP(ip)
+}
+
+func IP2HostID(ip net.IP) uint32 {
+	hostid := binary.BigEndian.Uint32(ip)
+	return hostid
 }

+ 177 - 31
pb/user.pb.go

@@ -11,6 +11,7 @@ It is generated from these files:
 It has these top-level messages:
 	UserListRequest
 	UserCreateRequest
+	UserUpdateRequest
 	UserDeleteRequest
 	UserRenewRequest
 	UserGenConfigRequest
@@ -18,10 +19,8 @@ It has these top-level messages:
 	UserGenConfigResponse
 	VPNStatusRequest
 	VPNInitRequest
-	VPNApplyRequest
 	VPNStatusResponse
 	VPNInitResponse
-	VPNApplyResponse
 */
 package pb
 
@@ -45,6 +44,30 @@ var _ = math.Inf
 // proto package needs to be updated.
 const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
 
+type UserUpdateRequest_GWPref int32
+
+const (
+	UserUpdateRequest_NOPREF UserUpdateRequest_GWPref = 0
+	UserUpdateRequest_NOGW   UserUpdateRequest_GWPref = 1
+	UserUpdateRequest_GW     UserUpdateRequest_GWPref = 2
+)
+
+var UserUpdateRequest_GWPref_name = map[int32]string{
+	0: "NOPREF",
+	1: "NOGW",
+	2: "GW",
+}
+var UserUpdateRequest_GWPref_value = map[string]int32{
+	"NOPREF": 0,
+	"NOGW":   1,
+	"GW":     2,
+}
+
+func (x UserUpdateRequest_GWPref) String() string {
+	return proto.EnumName(UserUpdateRequest_GWPref_name, int32(x))
+}
+func (UserUpdateRequest_GWPref) EnumDescriptor() ([]byte, []int) { return fileDescriptor0, []int{2, 0} }
+
 type UserListRequest struct {
 }
 
@@ -56,6 +79,8 @@ func (*UserListRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, [
 type UserCreateRequest struct {
 	Username string `protobuf:"bytes,1,opt,name=Username" json:"Username,omitempty"`
 	Password string `protobuf:"bytes,2,opt,name=Password" json:"Password,omitempty"`
+	NoGW     bool   `protobuf:"varint,3,opt,name=NoGW" json:"NoGW,omitempty"`
+	HostID   uint32 `protobuf:"varint,4,opt,name=HostID" json:"HostID,omitempty"`
 }
 
 func (m *UserCreateRequest) Reset()                    { *m = UserCreateRequest{} }
@@ -77,6 +102,60 @@ func (m *UserCreateRequest) GetPassword() string {
 	return ""
 }
 
+func (m *UserCreateRequest) GetNoGW() bool {
+	if m != nil {
+		return m.NoGW
+	}
+	return false
+}
+
+func (m *UserCreateRequest) GetHostID() uint32 {
+	if m != nil {
+		return m.HostID
+	}
+	return 0
+}
+
+type UserUpdateRequest struct {
+	Username string                   `protobuf:"bytes,1,opt,name=Username" json:"Username,omitempty"`
+	Password string                   `protobuf:"bytes,2,opt,name=Password" json:"Password,omitempty"`
+	Gwpref   UserUpdateRequest_GWPref `protobuf:"varint,3,opt,name=gwpref,enum=pb.UserUpdateRequest_GWPref" json:"gwpref,omitempty"`
+	HostID   uint32                   `protobuf:"varint,4,opt,name=HostID" json:"HostID,omitempty"`
+}
+
+func (m *UserUpdateRequest) Reset()                    { *m = UserUpdateRequest{} }
+func (m *UserUpdateRequest) String() string            { return proto.CompactTextString(m) }
+func (*UserUpdateRequest) ProtoMessage()               {}
+func (*UserUpdateRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} }
+
+func (m *UserUpdateRequest) GetUsername() string {
+	if m != nil {
+		return m.Username
+	}
+	return ""
+}
+
+func (m *UserUpdateRequest) GetPassword() string {
+	if m != nil {
+		return m.Password
+	}
+	return ""
+}
+
+func (m *UserUpdateRequest) GetGwpref() UserUpdateRequest_GWPref {
+	if m != nil {
+		return m.Gwpref
+	}
+	return UserUpdateRequest_NOPREF
+}
+
+func (m *UserUpdateRequest) GetHostID() uint32 {
+	if m != nil {
+		return m.HostID
+	}
+	return 0
+}
+
 type UserDeleteRequest struct {
 	Username string `protobuf:"bytes,1,opt,name=Username" json:"Username,omitempty"`
 }
@@ -84,7 +163,7 @@ type UserDeleteRequest struct {
 func (m *UserDeleteRequest) Reset()                    { *m = UserDeleteRequest{} }
 func (m *UserDeleteRequest) String() string            { return proto.CompactTextString(m) }
 func (*UserDeleteRequest) ProtoMessage()               {}
-func (*UserDeleteRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} }
+func (*UserDeleteRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{3} }
 
 func (m *UserDeleteRequest) GetUsername() string {
 	if m != nil {
@@ -100,7 +179,7 @@ type UserRenewRequest struct {
 func (m *UserRenewRequest) Reset()                    { *m = UserRenewRequest{} }
 func (m *UserRenewRequest) String() string            { return proto.CompactTextString(m) }
 func (*UserRenewRequest) ProtoMessage()               {}
-func (*UserRenewRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{3} }
+func (*UserRenewRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{4} }
 
 func (m *UserRenewRequest) GetUsername() string {
 	if m != nil {
@@ -116,7 +195,7 @@ type UserGenConfigRequest struct {
 func (m *UserGenConfigRequest) Reset()                    { *m = UserGenConfigRequest{} }
 func (m *UserGenConfigRequest) String() string            { return proto.CompactTextString(m) }
 func (*UserGenConfigRequest) ProtoMessage()               {}
-func (*UserGenConfigRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{4} }
+func (*UserGenConfigRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{5} }
 
 func (m *UserGenConfigRequest) GetUsername() string {
 	if m != nil {
@@ -132,7 +211,7 @@ type UserResponse struct {
 func (m *UserResponse) Reset()                    { *m = UserResponse{} }
 func (m *UserResponse) String() string            { return proto.CompactTextString(m) }
 func (*UserResponse) ProtoMessage()               {}
-func (*UserResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{5} }
+func (*UserResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{6} }
 
 func (m *UserResponse) GetUsers() []*UserResponse_User {
 	if m != nil {
@@ -146,12 +225,15 @@ type UserResponse_User struct {
 	ServerSerialNumber string `protobuf:"bytes,2,opt,name=ServerSerialNumber" json:"ServerSerialNumber,omitempty"`
 	Cert               string `protobuf:"bytes,3,opt,name=Cert" json:"Cert,omitempty"`
 	CreatedAt          string `protobuf:"bytes,4,opt,name=CreatedAt" json:"CreatedAt,omitempty"`
+	IPNet              string `protobuf:"bytes,5,opt,name=IPNet" json:"IPNet,omitempty"`
+	NoGW               bool   `protobuf:"varint,6,opt,name=NoGW" json:"NoGW,omitempty"`
+	HostID             uint32 `protobuf:"varint,7,opt,name=HostID" json:"HostID,omitempty"`
 }
 
 func (m *UserResponse_User) Reset()                    { *m = UserResponse_User{} }
 func (m *UserResponse_User) String() string            { return proto.CompactTextString(m) }
 func (*UserResponse_User) ProtoMessage()               {}
-func (*UserResponse_User) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{5, 0} }
+func (*UserResponse_User) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{6, 0} }
 
 func (m *UserResponse_User) GetUsername() string {
 	if m != nil {
@@ -181,6 +263,27 @@ func (m *UserResponse_User) GetCreatedAt() string {
 	return ""
 }
 
+func (m *UserResponse_User) GetIPNet() string {
+	if m != nil {
+		return m.IPNet
+	}
+	return ""
+}
+
+func (m *UserResponse_User) GetNoGW() bool {
+	if m != nil {
+		return m.NoGW
+	}
+	return false
+}
+
+func (m *UserResponse_User) GetHostID() uint32 {
+	if m != nil {
+		return m.HostID
+	}
+	return 0
+}
+
 type UserGenConfigResponse struct {
 	ClientConfig string `protobuf:"bytes,1,opt,name=ClientConfig" json:"ClientConfig,omitempty"`
 }
@@ -188,7 +291,7 @@ type UserGenConfigResponse struct {
 func (m *UserGenConfigResponse) Reset()                    { *m = UserGenConfigResponse{} }
 func (m *UserGenConfigResponse) String() string            { return proto.CompactTextString(m) }
 func (*UserGenConfigResponse) ProtoMessage()               {}
-func (*UserGenConfigResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{6} }
+func (*UserGenConfigResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{7} }
 
 func (m *UserGenConfigResponse) GetClientConfig() string {
 	if m != nil {
@@ -200,12 +303,14 @@ func (m *UserGenConfigResponse) GetClientConfig() string {
 func init() {
 	proto.RegisterType((*UserListRequest)(nil), "pb.UserListRequest")
 	proto.RegisterType((*UserCreateRequest)(nil), "pb.UserCreateRequest")
+	proto.RegisterType((*UserUpdateRequest)(nil), "pb.UserUpdateRequest")
 	proto.RegisterType((*UserDeleteRequest)(nil), "pb.UserDeleteRequest")
 	proto.RegisterType((*UserRenewRequest)(nil), "pb.UserRenewRequest")
 	proto.RegisterType((*UserGenConfigRequest)(nil), "pb.UserGenConfigRequest")
 	proto.RegisterType((*UserResponse)(nil), "pb.UserResponse")
 	proto.RegisterType((*UserResponse_User)(nil), "pb.UserResponse.User")
 	proto.RegisterType((*UserGenConfigResponse)(nil), "pb.UserGenConfigResponse")
+	proto.RegisterEnum("pb.UserUpdateRequest_GWPref", UserUpdateRequest_GWPref_name, UserUpdateRequest_GWPref_value)
 }
 
 // Reference imports to suppress errors if they are not otherwise used.
@@ -221,6 +326,7 @@ const _ = grpc.SupportPackageIsVersion4
 type UserServiceClient interface {
 	List(ctx context.Context, in *UserListRequest, opts ...grpc.CallOption) (*UserResponse, error)
 	Create(ctx context.Context, in *UserCreateRequest, opts ...grpc.CallOption) (*UserResponse, error)
+	Update(ctx context.Context, in *UserUpdateRequest, opts ...grpc.CallOption) (*UserResponse, error)
 	Delete(ctx context.Context, in *UserDeleteRequest, opts ...grpc.CallOption) (*UserResponse, error)
 	Renew(ctx context.Context, in *UserRenewRequest, opts ...grpc.CallOption) (*UserResponse, error)
 	GenConfig(ctx context.Context, in *UserGenConfigRequest, opts ...grpc.CallOption) (*UserGenConfigResponse, error)
@@ -252,6 +358,15 @@ func (c *userServiceClient) Create(ctx context.Context, in *UserCreateRequest, o
 	return out, nil
 }
 
+func (c *userServiceClient) Update(ctx context.Context, in *UserUpdateRequest, opts ...grpc.CallOption) (*UserResponse, error) {
+	out := new(UserResponse)
+	err := grpc.Invoke(ctx, "/pb.UserService/Update", in, out, c.cc, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
 func (c *userServiceClient) Delete(ctx context.Context, in *UserDeleteRequest, opts ...grpc.CallOption) (*UserResponse, error) {
 	out := new(UserResponse)
 	err := grpc.Invoke(ctx, "/pb.UserService/Delete", in, out, c.cc, opts...)
@@ -284,6 +399,7 @@ func (c *userServiceClient) GenConfig(ctx context.Context, in *UserGenConfigRequ
 type UserServiceServer interface {
 	List(context.Context, *UserListRequest) (*UserResponse, error)
 	Create(context.Context, *UserCreateRequest) (*UserResponse, error)
+	Update(context.Context, *UserUpdateRequest) (*UserResponse, error)
 	Delete(context.Context, *UserDeleteRequest) (*UserResponse, error)
 	Renew(context.Context, *UserRenewRequest) (*UserResponse, error)
 	GenConfig(context.Context, *UserGenConfigRequest) (*UserGenConfigResponse, error)
@@ -329,6 +445,24 @@ func _UserService_Create_Handler(srv interface{}, ctx context.Context, dec func(
 	return interceptor(ctx, in, info, handler)
 }
 
+func _UserService_Update_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(UserUpdateRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(UserServiceServer).Update(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/pb.UserService/Update",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(UserServiceServer).Update(ctx, req.(*UserUpdateRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
 func _UserService_Delete_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
 	in := new(UserDeleteRequest)
 	if err := dec(in); err != nil {
@@ -395,6 +529,10 @@ var _UserService_serviceDesc = grpc.ServiceDesc{
 			MethodName: "Create",
 			Handler:    _UserService_Create_Handler,
 		},
+		{
+			MethodName: "Update",
+			Handler:    _UserService_Update_Handler,
+		},
 		{
 			MethodName: "Delete",
 			Handler:    _UserService_Delete_Handler,
@@ -415,27 +553,35 @@ var _UserService_serviceDesc = grpc.ServiceDesc{
 func init() { proto.RegisterFile("user.proto", fileDescriptor0) }
 
 var fileDescriptor0 = []byte{
-	// 351 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x53, 0xd1, 0x4e, 0xea, 0x40,
-	0x10, 0xbd, 0x85, 0x42, 0x2e, 0x03, 0x89, 0x30, 0x42, 0xb2, 0x36, 0x3e, 0x90, 0x7d, 0x22, 0x31,
-	0x29, 0x11, 0x1e, 0x7d, 0xd2, 0x9a, 0xf8, 0xa0, 0x31, 0x06, 0xe2, 0x07, 0xb4, 0x32, 0x9a, 0x26,
-	0xb0, 0xad, 0xbb, 0x8b, 0xfc, 0x80, 0xff, 0xe1, 0xbf, 0xf8, 0x65, 0x66, 0xbb, 0x2d, 0x05, 0x52,
-	0x0d, 0x6f, 0x3b, 0xe7, 0xcc, 0x19, 0x66, 0x0e, 0xa7, 0x00, 0x6b, 0x45, 0xd2, 0x4f, 0x65, 0xa2,
-	0x13, 0xac, 0xa5, 0x11, 0xef, 0xc1, 0xc9, 0xb3, 0x22, 0xf9, 0x10, 0x2b, 0x3d, 0xa3, 0xf7, 0x35,
-	0x29, 0xcd, 0xef, 0xa1, 0x67, 0xa0, 0x40, 0x52, 0xa8, 0x29, 0x07, 0xd1, 0x83, 0xff, 0x06, 0x14,
-	0xe1, 0x8a, 0x98, 0x33, 0x74, 0x46, 0xad, 0xd9, 0xb6, 0x36, 0xdc, 0x53, 0xa8, 0xd4, 0x26, 0x91,
-	0x0b, 0x56, 0xb3, 0x5c, 0x51, 0xf3, 0xb1, 0x1d, 0x76, 0x4b, 0x4b, 0x3a, 0x6a, 0x18, 0xf7, 0xa1,
-	0x6b, 0xde, 0x33, 0x12, 0xb4, 0x39, 0xa6, 0x7f, 0x02, 0x7d, 0xf3, 0xbe, 0x23, 0x11, 0x24, 0xe2,
-	0x35, 0x7e, 0x3b, 0x46, 0xf3, 0xed, 0x40, 0xc7, 0xfe, 0x88, 0x4a, 0x13, 0xa1, 0x08, 0x2f, 0xa0,
-	0x61, 0x7c, 0x51, 0xcc, 0x19, 0xd6, 0x47, 0xed, 0xc9, 0xc0, 0x4f, 0x23, 0x7f, 0xb7, 0xc1, 0x16,
-	0xb6, 0xc7, 0xfb, 0x74, 0xc0, 0x35, 0xf5, 0x9f, 0x9e, 0xf8, 0x80, 0x73, 0x92, 0x1f, 0x24, 0xe7,
-	0x24, 0xe3, 0x70, 0xf9, 0xb8, 0x5e, 0x45, 0x24, 0x73, 0x77, 0x2a, 0x18, 0x44, 0x70, 0x03, 0x92,
-	0x9a, 0xd5, 0xb3, 0x8e, 0xec, 0x8d, 0xe7, 0xd0, 0xb2, 0x7f, 0xc2, 0xe2, 0x5a, 0x33, 0x37, 0x23,
-	0x4a, 0x80, 0x5f, 0xc1, 0xe0, 0xe0, 0xf0, 0xfc, 0x18, 0x0e, 0x9d, 0x60, 0x19, 0x93, 0xd0, 0x16,
-	0xcf, 0x57, 0xdb, 0xc3, 0x26, 0x5f, 0x35, 0x68, 0x1b, 0xb5, 0xd9, 0x24, 0x7e, 0x21, 0x1c, 0x83,
-	0x6b, 0x22, 0x80, 0xa7, 0xc5, 0xe5, 0x3b, 0x81, 0xf0, 0xba, 0x87, 0x76, 0xf0, 0x7f, 0x38, 0x85,
-	0xa6, 0x5d, 0x05, 0xb7, 0x66, 0xed, 0x05, 0xe6, 0x37, 0x91, 0x0d, 0x42, 0x29, 0xda, 0x0b, 0x46,
-	0xa5, 0xe8, 0x12, 0x1a, 0x59, 0x18, 0xb0, 0x5f, 0x92, 0x65, 0x36, 0x2a, 0x25, 0x37, 0xd0, 0xda,
-	0xda, 0x82, 0xac, 0x68, 0x38, 0x8c, 0x88, 0x77, 0x56, 0xc1, 0x14, 0x33, 0xa2, 0x66, 0xf6, 0x8d,
-	0x4c, 0x7f, 0x02, 0x00, 0x00, 0xff, 0xff, 0x31, 0xff, 0x3d, 0x2c, 0x31, 0x03, 0x00, 0x00,
+	// 480 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x54, 0xcd, 0x6e, 0xd3, 0x40,
+	0x10, 0xae, 0x5d, 0xc7, 0xc4, 0xd3, 0x02, 0xee, 0x90, 0x22, 0x13, 0xf5, 0x10, 0xf9, 0x80, 0x22,
+	0x21, 0xb9, 0x22, 0xe5, 0xc6, 0x09, 0x52, 0x30, 0x95, 0x90, 0x1b, 0x6d, 0x55, 0xe5, 0x9c, 0x90,
+	0x49, 0x65, 0x29, 0xb1, 0xcd, 0xee, 0x86, 0xbc, 0x00, 0x2f, 0xc6, 0x85, 0x27, 0xe0, 0x81, 0xd0,
+	0x7a, 0xfd, 0x83, 0x83, 0x83, 0x72, 0xe0, 0x36, 0x7f, 0xdf, 0x8e, 0xe7, 0x9b, 0x6f, 0x0c, 0xb0,
+	0x11, 0xc4, 0x83, 0x8c, 0xa7, 0x32, 0x45, 0x33, 0x9b, 0xfb, 0x67, 0xf0, 0xf4, 0x5e, 0x10, 0xff,
+	0x1c, 0x0b, 0xc9, 0xe8, 0xeb, 0x86, 0x84, 0xf4, 0xb7, 0x70, 0xa6, 0x42, 0x63, 0x4e, 0x33, 0x49,
+	0x45, 0x10, 0xfb, 0xd0, 0x55, 0xc1, 0x64, 0xb6, 0x26, 0xcf, 0x18, 0x18, 0x43, 0x87, 0x55, 0xbe,
+	0xca, 0x4d, 0x66, 0x42, 0x6c, 0x53, 0xbe, 0xf0, 0x4c, 0x9d, 0x2b, 0x7d, 0x44, 0xb0, 0xa2, 0x34,
+	0x9c, 0x7a, 0xc7, 0x03, 0x63, 0xd8, 0x65, 0xb9, 0x8d, 0xcf, 0xc1, 0xfe, 0x94, 0x0a, 0x79, 0x73,
+	0xed, 0x59, 0x03, 0x63, 0xf8, 0x98, 0x15, 0x9e, 0xff, 0xc3, 0xd0, 0x9d, 0xef, 0xb3, 0xc5, 0x7f,
+	0xe8, 0xfc, 0x06, 0xec, 0x87, 0x6d, 0xc6, 0x69, 0x99, 0xf7, 0x7e, 0x32, 0xba, 0x08, 0xb2, 0x79,
+	0xf0, 0xd7, 0xf3, 0x41, 0x38, 0x9d, 0x70, 0x5a, 0xb2, 0xa2, 0x76, 0xef, 0xb7, 0xbd, 0x04, 0x5b,
+	0x57, 0x22, 0x80, 0x1d, 0xdd, 0x4e, 0xd8, 0x87, 0x8f, 0xee, 0x11, 0x76, 0xc1, 0x8a, 0x6e, 0xc3,
+	0xa9, 0x6b, 0xa0, 0x0d, 0x66, 0x38, 0x75, 0x4d, 0xff, 0x52, 0x8f, 0x70, 0x4d, 0x2b, 0x3a, 0x68,
+	0x04, 0x3f, 0x00, 0x57, 0xd9, 0x8c, 0x12, 0xda, 0x1e, 0x52, 0x3f, 0x82, 0x9e, 0xb2, 0x43, 0x4a,
+	0xc6, 0x69, 0xb2, 0x8c, 0x1f, 0x0e, 0xc1, 0x7c, 0x37, 0xe1, 0x54, 0x37, 0x11, 0x59, 0x9a, 0x08,
+	0xc2, 0x57, 0xd0, 0x51, 0x3a, 0x10, 0x9e, 0x31, 0x38, 0x1e, 0x9e, 0x8c, 0xce, 0x4b, 0x6a, 0xca,
+	0x02, 0xed, 0xe8, 0x9a, 0xfe, 0x4f, 0x03, 0x2c, 0xe5, 0xff, 0x73, 0x13, 0x01, 0xe0, 0x1d, 0xf1,
+	0x6f, 0xc4, 0xef, 0x88, 0xc7, 0xb3, 0x55, 0xb4, 0x59, 0xcf, 0x89, 0x17, 0x3b, 0x69, 0xc9, 0x28,
+	0x5d, 0x8c, 0x89, 0xcb, 0x7c, 0x37, 0x0e, 0xcb, 0x6d, 0xbc, 0x00, 0x47, 0x8b, 0x6e, 0xf1, 0x4e,
+	0xe6, 0xf4, 0x3b, 0xac, 0x0e, 0x60, 0x0f, 0x3a, 0x37, 0x93, 0x88, 0xa4, 0xd7, 0xc9, 0x33, 0xda,
+	0xa9, 0xf4, 0x65, 0xb7, 0xea, 0xeb, 0x51, 0x63, 0x87, 0x6f, 0xe1, 0x7c, 0x87, 0xba, 0x82, 0x0e,
+	0x1f, 0x4e, 0xc7, 0xab, 0x98, 0x12, 0xa9, 0xe3, 0xc5, 0x70, 0x8d, 0xd8, 0xe8, 0x97, 0x09, 0x27,
+	0x0a, 0xad, 0x66, 0x89, 0xbf, 0x10, 0x5e, 0x82, 0xa5, 0x8e, 0x06, 0x9f, 0x95, 0xdc, 0xfd, 0x71,
+	0x42, 0x7d, 0x77, 0x97, 0x50, 0xff, 0x08, 0xaf, 0xc0, 0xd6, 0xc3, 0x60, 0x45, 0x77, 0xe3, 0xc4,
+	0xf6, 0x81, 0xb4, 0x5c, 0x6b, 0x50, 0x43, 0xbe, 0xfb, 0x40, 0x5a, 0x7f, 0x35, 0xa8, 0xa1, 0xc7,
+	0x56, 0xd0, 0x6b, 0xe8, 0xe4, 0x1a, 0xc4, 0x5e, 0x9d, 0xac, 0x25, 0xd9, 0x0a, 0x79, 0x0f, 0x4e,
+	0xc5, 0x25, 0x7a, 0x65, 0xc1, 0xae, 0x32, 0xfb, 0x2f, 0x5a, 0x32, 0xe5, 0x1b, 0x73, 0x3b, 0xff,
+	0x15, 0x5d, 0xfd, 0x0e, 0x00, 0x00, 0xff, 0xff, 0x90, 0xef, 0x0f, 0xc4, 0x98, 0x04, 0x00, 0x00,
 }

+ 19 - 0
pb/user.proto

@@ -9,8 +9,23 @@ message UserListRequest {
 message UserCreateRequest {
   string Username = 1;
   string Password = 2;
+  bool NoGW = 3;
+  uint32 HostID = 4;
 }
 
+message UserUpdateRequest {
+  string Username = 1;
+  string Password = 2;
+  enum GWPref {
+    NOPREF = 0;
+    NOGW = 1;
+    GW = 2;
+  }
+  GWPref gwpref = 3;
+  uint32 HostID = 4;
+}
+
+
 message UserDeleteRequest {
   string Username = 1;
 }
@@ -26,6 +41,7 @@ message UserGenConfigRequest {
 service UserService {
   rpc List (UserListRequest) returns (UserResponse) {}
   rpc Create (UserCreateRequest) returns (UserResponse) {}
+  rpc Update (UserUpdateRequest) returns (UserResponse) {}
   rpc Delete (UserDeleteRequest) returns (UserResponse) {}
   rpc Renew (UserRenewRequest) returns (UserResponse) {}
   rpc GenConfig (UserGenConfigRequest) returns (UserGenConfigResponse) {}
@@ -37,6 +53,9 @@ message UserResponse {
     string ServerSerialNumber = 2;
     string Cert = 3;
     string CreatedAt = 4;
+    string IPNet = 5;
+    bool NoGW = 6;
+    uint32 HostID = 7;
   }
 
   repeated User users = 1;

+ 21 - 74
pb/vpn.pb.go

@@ -49,14 +49,6 @@ func (m *VPNInitRequest) GetPort() string {
 	return ""
 }
 
-type VPNApplyRequest struct {
-}
-
-func (m *VPNApplyRequest) Reset()                    { *m = VPNApplyRequest{} }
-func (m *VPNApplyRequest) String() string            { return proto.CompactTextString(m) }
-func (*VPNApplyRequest) ProtoMessage()               {}
-func (*VPNApplyRequest) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{2} }
-
 type VPNStatusResponse struct {
 	Name         string `protobuf:"bytes,1,opt,name=Name" json:"Name,omitempty"`
 	SerialNumber string `protobuf:"bytes,2,opt,name=SerialNumber" json:"SerialNumber,omitempty"`
@@ -72,7 +64,7 @@ type VPNStatusResponse struct {
 func (m *VPNStatusResponse) Reset()                    { *m = VPNStatusResponse{} }
 func (m *VPNStatusResponse) String() string            { return proto.CompactTextString(m) }
 func (*VPNStatusResponse) ProtoMessage()               {}
-func (*VPNStatusResponse) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{3} }
+func (*VPNStatusResponse) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{2} }
 
 func (m *VPNStatusResponse) GetName() string {
 	if m != nil {
@@ -143,23 +135,13 @@ type VPNInitResponse struct {
 func (m *VPNInitResponse) Reset()                    { *m = VPNInitResponse{} }
 func (m *VPNInitResponse) String() string            { return proto.CompactTextString(m) }
 func (*VPNInitResponse) ProtoMessage()               {}
-func (*VPNInitResponse) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{4} }
-
-type VPNApplyResponse struct {
-}
-
-func (m *VPNApplyResponse) Reset()                    { *m = VPNApplyResponse{} }
-func (m *VPNApplyResponse) String() string            { return proto.CompactTextString(m) }
-func (*VPNApplyResponse) ProtoMessage()               {}
-func (*VPNApplyResponse) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{5} }
+func (*VPNInitResponse) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{3} }
 
 func init() {
 	proto.RegisterType((*VPNStatusRequest)(nil), "pb.VPNStatusRequest")
 	proto.RegisterType((*VPNInitRequest)(nil), "pb.VPNInitRequest")
-	proto.RegisterType((*VPNApplyRequest)(nil), "pb.VPNApplyRequest")
 	proto.RegisterType((*VPNStatusResponse)(nil), "pb.VPNStatusResponse")
 	proto.RegisterType((*VPNInitResponse)(nil), "pb.VPNInitResponse")
-	proto.RegisterType((*VPNApplyResponse)(nil), "pb.VPNApplyResponse")
 }
 
 // Reference imports to suppress errors if they are not otherwise used.
@@ -175,7 +157,6 @@ const _ = grpc.SupportPackageIsVersion4
 type VPNServiceClient interface {
 	Status(ctx context.Context, in *VPNStatusRequest, opts ...grpc.CallOption) (*VPNStatusResponse, error)
 	Init(ctx context.Context, in *VPNInitRequest, opts ...grpc.CallOption) (*VPNInitResponse, error)
-	Apply(ctx context.Context, in *VPNApplyRequest, opts ...grpc.CallOption) (*VPNApplyResponse, error)
 }
 
 type vPNServiceClient struct {
@@ -204,21 +185,11 @@ func (c *vPNServiceClient) Init(ctx context.Context, in *VPNInitRequest, opts ..
 	return out, nil
 }
 
-func (c *vPNServiceClient) Apply(ctx context.Context, in *VPNApplyRequest, opts ...grpc.CallOption) (*VPNApplyResponse, error) {
-	out := new(VPNApplyResponse)
-	err := grpc.Invoke(ctx, "/pb.VPNService/Apply", in, out, c.cc, opts...)
-	if err != nil {
-		return nil, err
-	}
-	return out, nil
-}
-
 // Server API for VPNService service
 
 type VPNServiceServer interface {
 	Status(context.Context, *VPNStatusRequest) (*VPNStatusResponse, error)
 	Init(context.Context, *VPNInitRequest) (*VPNInitResponse, error)
-	Apply(context.Context, *VPNApplyRequest) (*VPNApplyResponse, error)
 }
 
 func RegisterVPNServiceServer(s *grpc.Server, srv VPNServiceServer) {
@@ -261,24 +232,6 @@ func _VPNService_Init_Handler(srv interface{}, ctx context.Context, dec func(int
 	return interceptor(ctx, in, info, handler)
 }
 
-func _VPNService_Apply_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
-	in := new(VPNApplyRequest)
-	if err := dec(in); err != nil {
-		return nil, err
-	}
-	if interceptor == nil {
-		return srv.(VPNServiceServer).Apply(ctx, in)
-	}
-	info := &grpc.UnaryServerInfo{
-		Server:     srv,
-		FullMethod: "/pb.VPNService/Apply",
-	}
-	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
-		return srv.(VPNServiceServer).Apply(ctx, req.(*VPNApplyRequest))
-	}
-	return interceptor(ctx, in, info, handler)
-}
-
 var _VPNService_serviceDesc = grpc.ServiceDesc{
 	ServiceName: "pb.VPNService",
 	HandlerType: (*VPNServiceServer)(nil),
@@ -291,10 +244,6 @@ var _VPNService_serviceDesc = grpc.ServiceDesc{
 			MethodName: "Init",
 			Handler:    _VPNService_Init_Handler,
 		},
-		{
-			MethodName: "Apply",
-			Handler:    _VPNService_Apply_Handler,
-		},
 	},
 	Streams:  []grpc.StreamDesc{},
 	Metadata: "vpn.proto",
@@ -303,25 +252,23 @@ var _VPNService_serviceDesc = grpc.ServiceDesc{
 func init() { proto.RegisterFile("vpn.proto", fileDescriptor1) }
 
 var fileDescriptor1 = []byte{
-	// 312 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x92, 0xcb, 0x4e, 0xf3, 0x30,
-	0x10, 0x85, 0xff, 0xf4, 0x92, 0xbf, 0x19, 0x21, 0x68, 0x87, 0x82, 0xac, 0x88, 0x05, 0xca, 0x8a,
-	0x55, 0x25, 0x2e, 0x12, 0x5b, 0xa2, 0x6c, 0x60, 0x81, 0x15, 0xb5, 0x52, 0xf6, 0x09, 0x78, 0x11,
-	0xd1, 0x26, 0xc6, 0x76, 0x2a, 0xf1, 0x52, 0xbc, 0x18, 0x2f, 0x81, 0x7c, 0x49, 0x93, 0x54, 0x62,
-	0x77, 0xfc, 0x39, 0x67, 0x66, 0x72, 0xc6, 0x10, 0xec, 0x79, 0xb5, 0xe2, 0xa2, 0x56, 0x35, 0x8e,
-	0x78, 0x11, 0x21, 0xcc, 0xb3, 0x94, 0x6e, 0x54, 0xae, 0x1a, 0xb9, 0x66, 0x9f, 0x0d, 0x93, 0x2a,
-	0x7a, 0x82, 0xd3, 0x2c, 0xa5, 0x2f, 0x55, 0xa9, 0x1c, 0xc1, 0x10, 0x66, 0xcf, 0xb5, 0x54, 0x55,
-	0xbe, 0x63, 0xc4, 0xbb, 0xf6, 0x6e, 0x82, 0xf5, 0xe1, 0x8c, 0x08, 0x93, 0xb4, 0x16, 0x8a, 0x8c,
-	0x0c, 0x37, 0x3a, 0x5a, 0xc0, 0x59, 0x96, 0xd2, 0x98, 0xf3, 0xed, 0x57, 0x5b, 0xf4, 0xc7, 0x83,
-	0x45, 0xaf, 0x93, 0xe4, 0x75, 0x25, 0x8d, 0x99, 0x76, 0x45, 0x8d, 0xc6, 0x08, 0x4e, 0x36, 0x4c,
-	0x94, 0xf9, 0x96, 0x36, 0xbb, 0x82, 0x09, 0x57, 0x78, 0xc0, 0x06, 0x03, 0x8d, 0xff, 0x18, 0x68,
-	0xd2, 0x0d, 0xa4, 0x59, 0xc2, 0x84, 0x22, 0x53, 0xcb, 0xb4, 0xc6, 0x4b, 0xf0, 0x93, 0xd8, 0x50,
-	0xdf, 0x50, 0x77, 0xc2, 0x39, 0x8c, 0x29, 0x53, 0xe4, 0xbf, 0x81, 0x5a, 0x6a, 0xf7, 0x6b, 0x2e,
-	0x3f, 0xc8, 0xcc, 0xba, 0xb5, 0xc6, 0x2b, 0x08, 0x12, 0xc1, 0x72, 0xc5, 0xde, 0x63, 0x45, 0x02,
-	0x73, 0xd1, 0x01, 0x17, 0x80, 0x8d, 0xd0, 0xfe, 0xaa, 0x4b, 0xda, 0x65, 0x62, 0xd9, 0xdd, 0xb7,
-	0x07, 0xa0, 0x43, 0x61, 0x62, 0x5f, 0xbe, 0x31, 0x7c, 0x04, 0xdf, 0xe6, 0x83, 0xcb, 0x15, 0x2f,
-	0x56, 0xc7, 0x8b, 0x09, 0x2f, 0x8e, 0xa8, 0xab, 0xfc, 0x0f, 0x6f, 0x61, 0xa2, 0x7b, 0x21, 0xba,
-	0x0f, 0x7a, 0xbb, 0x0b, 0xcf, 0x07, 0xec, 0x60, 0x79, 0x80, 0xa9, 0x99, 0x05, 0xdb, 0xfb, 0xfe,
-	0xb6, 0xc2, 0xe5, 0x10, 0xb6, 0xae, 0xc2, 0x37, 0x2f, 0xe7, 0xfe, 0x37, 0x00, 0x00, 0xff, 0xff,
-	0x10, 0x88, 0xf5, 0x13, 0x46, 0x02, 0x00, 0x00,
+	// 278 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x91, 0x4f, 0x4f, 0x83, 0x40,
+	0x10, 0xc5, 0xa5, 0x45, 0x2c, 0x13, 0xa3, 0xed, 0xf8, 0x27, 0x1b, 0xe2, 0xc1, 0xec, 0xc9, 0x13,
+	0x89, 0x7a, 0xf0, 0x6a, 0xc3, 0x45, 0x0f, 0x12, 0xd2, 0x26, 0xbd, 0x83, 0xce, 0x81, 0x68, 0x01,
+	0x77, 0x87, 0xc6, 0xef, 0xec, 0x97, 0x30, 0xbb, 0x8b, 0xa5, 0x34, 0xe9, 0xed, 0xed, 0x6f, 0x67,
+	0xde, 0x64, 0xde, 0x40, 0xb8, 0x69, 0xaa, 0xb8, 0x51, 0x35, 0xd7, 0x38, 0x6a, 0x0a, 0x89, 0x30,
+	0x5d, 0x65, 0xe9, 0x92, 0x73, 0x6e, 0xf5, 0x82, 0xbe, 0x5b, 0xd2, 0x2c, 0x9f, 0xe1, 0x6c, 0x95,
+	0xa5, 0xaf, 0x55, 0xc9, 0x1d, 0xc1, 0x08, 0x26, 0x2f, 0xb5, 0xe6, 0x2a, 0x5f, 0x93, 0xf0, 0x6e,
+	0xbd, 0xbb, 0x70, 0xb1, 0x7d, 0x23, 0x82, 0x9f, 0xd5, 0x8a, 0xc5, 0xc8, 0x72, 0xab, 0xe5, 0xaf,
+	0x07, 0xb3, 0x1d, 0x5b, 0xdd, 0xd4, 0x95, 0xb6, 0x95, 0x69, 0xef, 0x60, 0x35, 0x4a, 0x38, 0x5d,
+	0x92, 0x2a, 0xf3, 0xaf, 0xb4, 0x5d, 0x17, 0xa4, 0x3a, 0x97, 0x01, 0x1b, 0x4c, 0x1f, 0x1f, 0x98,
+	0xee, 0xf7, 0xd3, 0x0d, 0x4b, 0x48, 0xb1, 0x38, 0x76, 0xcc, 0x68, 0xbc, 0x86, 0x20, 0x99, 0x5b,
+	0x1a, 0x58, 0xda, 0xbd, 0x70, 0x0a, 0xe3, 0x94, 0x58, 0x9c, 0x58, 0x68, 0xa4, 0xe9, 0x7e, 0xcb,
+	0xf5, 0xa7, 0x98, 0xb8, 0x6e, 0xa3, 0xf1, 0x06, 0xc2, 0x44, 0x51, 0xce, 0xf4, 0x31, 0x67, 0x11,
+	0xda, 0x8f, 0x1e, 0xc8, 0x19, 0x9c, 0x6f, 0xf3, 0x72, 0xab, 0x3e, 0xfc, 0x00, 0x98, 0xfd, 0x49,
+	0x6d, 0xca, 0x77, 0xc2, 0x27, 0x08, 0x5c, 0x14, 0x78, 0x19, 0x37, 0x45, 0xbc, 0x1f, 0x78, 0x74,
+	0xb5, 0x47, 0x9d, 0x89, 0x3c, 0xc2, 0x7b, 0xf0, 0x8d, 0x2d, 0x62, 0x57, 0xb0, 0x73, 0x93, 0xe8,
+	0x62, 0xc0, 0xfe, 0x5b, 0x8a, 0xc0, 0xde, 0xf6, 0xf1, 0x2f, 0x00, 0x00, 0xff, 0xff, 0xc0, 0x1d,
+	0xb6, 0x7e, 0xe8, 0x01, 0x00, 0x00,
 }

+ 0 - 3
pb/vpn.proto

@@ -7,12 +7,10 @@ message VPNInitRequest {
   string Hostname = 1;
   string Port = 2;
 }
-message VPNApplyRequest {}
 
 service VPNService {
   rpc Status (VPNStatusRequest) returns (VPNStatusResponse) {}
   rpc Init (VPNInitRequest) returns (VPNInitResponse) {}
-  rpc Apply (VPNApplyRequest) returns (VPNApplyResponse) {}
 }
 
 message VPNStatusResponse {
@@ -27,4 +25,3 @@ message VPNStatusResponse {
   string CreatedAt = 9;
 }
 message VPNInitResponse {}
-message VPNApplyResponse {}

+ 19 - 2
pki/pki.go

@@ -85,7 +85,7 @@ func NewCA() (*CA, error) {
 		SerialNumber:          serial,
 		Subject:               names,
 		NotBefore:             now.Add(-10 * time.Minute).UTC(),
-		NotAfter:              now.Add(time.Duration(24*365) * time.Hour).UTC(),
+		NotAfter:              now.Add(time.Duration(24*365*_CrtExpireYears) * time.Hour).UTC(),
 		BasicConstraintsValid: true,
 		IsCA:     true,
 		KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
@@ -156,10 +156,15 @@ func newCert(ca *CA, server bool, cn string) (*CertHolder, error) {
 		return nil, err
 	}
 
+	val, err := asn1.Marshal(asn1.BitString{Bytes: []byte{0x80}, BitLength: 2}) // setting nsCertType to Client Type
+	if err != nil {
+		return nil, fmt.Errorf("can not marshal nsCertType: %v", err)
+	}
+
 	now := time.Now()
 	tml := x509.Certificate{
 		NotBefore:    now.Add(-10 * time.Minute).UTC(),
-		NotAfter:     now.Add(time.Duration(24*365) * time.Hour).UTC(),
+		NotAfter:     now.Add(time.Duration(24*365*_CrtExpireYears) * time.Hour).UTC(),
 		SerialNumber: serial,
 		Subject: pkix.Name{
 			CommonName:   cn,
@@ -168,11 +173,23 @@ func newCert(ca *CA, server bool, cn string) (*CertHolder, error) {
 		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageKeyAgreement,
 		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
 		BasicConstraintsValid: true,
+		ExtraExtensions: []pkix.Extension{
+			{
+				Id:    asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 1, 1},
+				Value: val,
+			},
+		},
 	}
 
 	if server {
 		tml.KeyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageKeyAgreement | x509.KeyUsageKeyEncipherment
 		tml.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}
+		val, err := asn1.Marshal(asn1.BitString{Bytes: []byte{0x40}, BitLength: 2}) // setting nsCertType to Server Type
+		if err != nil {
+			return nil, fmt.Errorf("can not marshal nsCertType: %v", err)
+		}
+		tml.ExtraExtensions[0].Id = asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 1, 1}
+		tml.ExtraExtensions[0].Value = val
 	}
 
 	// Sign with CA's private key

+ 5 - 0
template/client.ovpn.tmpl

@@ -6,6 +6,7 @@ proto udp
 remote {{ .Hostname }} {{ .Port }}
 resolv-retry infinite
 remote-cert-tls server
+ns-cert-type server
 nobind
 persist-key
 persist-tun
@@ -18,3 +19,7 @@ verb 3
 {{ .Cert }}</cert>
 <key>
 {{ .Key }}</key>
+
+{{ if .NoGW }}
+route-nopull
+{{ end }}

+ 189 - 13
user.go

@@ -2,8 +2,11 @@ package ovpm
 
 import (
 	"fmt"
+	"net"
 	"time"
 
+	passlib "gopkg.in/hlandau/passlib.v1"
+
 	"github.com/Sirupsen/logrus"
 	"github.com/asaskevich/govalidator"
 	"github.com/cad/ovpm/pki"
@@ -15,6 +18,9 @@ type User interface {
 	GetUsername() string
 	GetServerSerialNumber() string
 	GetCert() string
+	GetIPNet() string
+	IsNoGW() bool
+	GetHostID() uint32
 }
 
 // DBUser is database model for VPN users.
@@ -24,10 +30,12 @@ type DBUser struct {
 	Server   DBServer
 
 	Username           string `gorm:"unique_index"`
-	Cert               string
-	ServerSerialNumber string
-	Password           string
-	Key                string
+	Cert               string // not user writable
+	ServerSerialNumber string // not user writable
+	Hash               string
+	Key                string // not user writable
+	NoGW               bool
+	HostID             uint32 // not user writable
 }
 
 // DBRevoked is a database model for revoked VPN users.
@@ -37,14 +45,23 @@ type DBRevoked struct {
 }
 
 func (u *DBUser) setPassword(password string) error {
-	// TODO(cad): Use a proper password hashing algorithm here.
-	u.Password = password
+	hashedPassword, err := passlib.Hash(password)
+	if err != nil {
+		return fmt.Errorf("can not set password: %v", err)
+	}
+
+	u.Hash = hashedPassword
 	return nil
 }
 
 // CheckPassword returns wether the given password is correct for the user.
 func (u *DBUser) CheckPassword(password string) bool {
-	return u.Password == password
+	_, err := passlib.Verify(password, u.Hash)
+	if err != nil {
+		logrus.Error(err)
+		return false
+	}
+	return true
 }
 
 // GetUser finds and returns the user with the given username from database.
@@ -68,9 +85,11 @@ func GetAllUsers() ([]*DBUser, error) {
 }
 
 // CreateNewUser creates a new user with the given username and password in the database.
+// If nogw is true, then ovpm doesn't push vpn server as the default gw for the user.
+//
 // It also generates the necessary client keys and signs certificates with the current
 // server's CA.
-func CreateNewUser(username, password string) (*DBUser, error) {
+func CreateNewUser(username, password string, nogw bool, hostid uint32) (*DBUser, error) {
 	if !IsInitialized() {
 		return nil, fmt.Errorf("you first need to create server")
 	}
@@ -81,6 +100,7 @@ func CreateNewUser(username, password string) (*DBUser, error) {
 	if !govalidator.IsAlphanumeric(username) {
 		return nil, fmt.Errorf("validation error: `%s` can only contain letters and numbers", username)
 	}
+
 	ca, err := GetSystemCA()
 	if err != nil {
 		return nil, err
@@ -94,14 +114,31 @@ func CreateNewUser(username, password string) (*DBUser, error) {
 	if err != nil {
 		return nil, fmt.Errorf("can not get server: %v", err)
 	}
-	user := DBUser{
 
+	if hostid != 0 {
+		ip := HostID2IP(hostid)
+		if ip == nil {
+			return nil, fmt.Errorf("host id doesn't represent an ip %d", hostid)
+		}
+
+		network := net.IPNet{IP: net.ParseIP(server.Net).To4(), Mask: net.IPMask(net.ParseIP(server.Mask).To4())}
+		if !network.Contains(ip) {
+			return nil, fmt.Errorf("ip %s, is out of vpn network %s", ip, network.String())
+		}
+
+		if hostIDsContains(getStaticHostIDs(), hostid) {
+			return nil, fmt.Errorf("ip %s is already allocated", ip)
+		}
+	}
+	user := DBUser{
 		Username:           username,
-		Password:           password,
 		Cert:               clientCert.Cert,
 		Key:                clientCert.Key,
 		ServerSerialNumber: server.SerialNumber,
+		NoGW:               nogw,
+		HostID:             hostid,
 	}
+	user.setPassword(password)
 
 	db.Create(&user)
 	if db.NewRecord(&user) {
@@ -118,9 +155,54 @@ func CreateNewUser(username, password string) (*DBUser, error) {
 	return &user, nil
 }
 
+// Update updates the user's attributes and writes them to the database.
+//
+// How this method works is similiar to PUT semantics of REST. It sets the user record fields to the provided function arguments.
+func (u *DBUser) Update(password string, nogw bool, hostid uint32) error {
+	if !IsInitialized() {
+		return fmt.Errorf("you first need to create server")
+	}
+
+	// If password is provided; set it. If not; leave it as it is.
+	if password != "" {
+		u.setPassword(password)
+	}
+
+	u.NoGW = nogw
+	u.HostID = hostid
+	db.Save(u)
+
+	if hostid != 0 {
+		server, err := GetServerInstance()
+		if err != nil {
+			return fmt.Errorf("can not get server: %v", err)
+		}
+
+		ip := HostID2IP(hostid)
+		if ip == nil {
+			return fmt.Errorf("host id doesn't represent an ip %d", hostid)
+		}
+
+		network := net.IPNet{IP: net.ParseIP(server.Net).To4(), Mask: net.IPMask(net.ParseIP(server.Mask).To4())}
+		if !network.Contains(ip) {
+			return fmt.Errorf("ip %s, is out of vpn network %s", ip, network.String())
+		}
+
+		if hostIDsContains(getStaticHostIDs(), hostid) {
+			return fmt.Errorf("ip %s is already allocated", ip)
+		}
+	}
+
+	err := Emit()
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
 // Delete deletes a user by the given username from the database.
 func (u *DBUser) Delete() error {
-	if db.NewRecord(&u) {
+	if db.NewRecord(u) {
 		// user is not found
 		return fmt.Errorf("user is not initialized: %s", u.Username)
 	}
@@ -131,7 +213,7 @@ func (u *DBUser) Delete() error {
 	db.Create(&DBRevoked{
 		SerialNumber: crt.SerialNumber.Text(16),
 	})
-	db.Unscoped().Delete(&u)
+	db.Unscoped().Delete(u)
 	logrus.Infof("user deleted: %s", u.GetUsername())
 	err = Emit()
 	if err != nil {
@@ -149,6 +231,11 @@ func (u *DBUser) ResetPassword(password string) error {
 		return fmt.Errorf("user password can not be updated %s: %v", u.Username, err)
 	}
 	db.Save(u)
+	err = Emit()
+	if err != nil {
+		return err
+	}
+
 	logrus.Infof("user password reset: %s", u.GetUsername())
 	return nil
 }
@@ -180,7 +267,12 @@ func (u *DBUser) Renew() error {
 	u.Key = clientCert.Key
 	u.ServerSerialNumber = server.SerialNumber
 
-	db.Save(&u)
+	db.Save(u)
+	err = Emit()
+	if err != nil {
+		return err
+	}
+
 	logrus.Infof("user renewed cert: %s", u.GetUsername())
 	return nil
 }
@@ -204,3 +296,87 @@ func (u *DBUser) GetServerSerialNumber() string {
 func (u *DBUser) GetCreatedAt() string {
 	return u.CreatedAt.Format(time.UnixDate)
 }
+
+// getIP returns user's vpn ip addr.
+func (u *DBUser) getIP() net.IP {
+	users := getNonStaticHostUsers()
+	staticHostIDs := getStaticHostIDs()
+	mask := net.IPMask(net.ParseIP(_DefaultServerNetMask).To4())
+	network := net.ParseIP(_DefaultServerNetwork).To4().Mask(mask)
+
+	// Host is static?
+	if u.HostID != 0 {
+		// Host is really static?
+		if hostIDsContains(staticHostIDs, u.HostID) {
+			return HostID2IP(u.HostID)
+		}
+		return nil
+	}
+
+	// Host is dynamic.
+	for i, user := range users {
+		hostID := IP2HostID(network) + uint32(i+2)
+		if hostIDsContains(staticHostIDs, hostID) {
+			for hostIDsContains(staticHostIDs, hostID) {
+				i++
+				hostID = IP2HostID(network) + uint32(i+1)
+			}
+		}
+		if user.ID == u.ID {
+			return HostID2IP(hostID)
+		}
+	}
+	return nil
+}
+
+// GetIPNet returns user's vpn ip network. (e.g. 192.168.0.1/24)
+func (u *DBUser) GetIPNet() string {
+	mask := net.IPMask(net.ParseIP(_DefaultServerNetMask).To4())
+
+	ipn := net.IPNet{
+		IP:   u.getIP(),
+		Mask: mask,
+	}
+	return ipn.String()
+}
+
+// IsNoGW returns wether user is set to get the vpn server as their default gateway.
+func (u *DBUser) IsNoGW() bool {
+	return u.NoGW
+}
+
+// GetHostID returns user's Host ID.
+func (u *DBUser) GetHostID() uint32 {
+	return u.HostID
+}
+
+func getStaticHostUsers() []*DBUser {
+	var users []*DBUser
+	db.Unscoped().Not(DBUser{HostID: 0}).Find(&users)
+	return users
+}
+
+func getNonStaticHostUsers() []*DBUser {
+	var users []*DBUser
+	db.Unscoped().Where(DBUser{HostID: 0}).Find(&users)
+	return users
+}
+
+func getStaticHostIDs() []uint32 {
+	var ids []uint32
+	users := getStaticHostUsers()
+	for _, user := range users {
+		ids = append(ids, user.HostID)
+	}
+
+	return ids
+}
+
+func hostIDsContains(s []uint32, e uint32) bool {
+	for _, a := range s {
+		if a == e {
+			return true
+		}
+	}
+	return false
+}

+ 97 - 10
user_test.go

@@ -2,6 +2,7 @@ package ovpm_test
 
 import (
 	"fmt"
+	"net"
 	"testing"
 
 	"github.com/Sirupsen/logrus"
@@ -19,9 +20,10 @@ func TestCreateNewUser(t *testing.T) {
 	// Prepare:
 	username := "testUser"
 	password := "testPasswd1234"
+	noGW := false
 
 	// Test:
-	user, err := ovpm.CreateNewUser(username, password)
+	user, err := ovpm.CreateNewUser(username, password, noGW, 0)
 	if err != nil {
 		t.Fatalf("user can not be created: %v", err)
 	}
@@ -63,6 +65,54 @@ func TestCreateNewUser(t *testing.T) {
 		t.Errorf("user.GetCert() is expected to return '%s' but it returns '%s' %+v", user.Cert, user.GetCert(), user)
 	}
 
+	user.Delete()
+
+	// Is NoGW attr working properly?
+	noGW = true
+	user, err = ovpm.CreateNewUser(username, password, noGW, 0)
+	if err != nil {
+		t.Fatalf("user can not be created: %v", err)
+	}
+	if user.NoGW != noGW {
+		t.Fatalf("user.NoGW is expected to be %t but it's %t instead", noGW, user.NoGW)
+	}
+}
+
+func TestUserUpdate(t *testing.T) {
+	// Initialize:
+	ovpm.Testing = true
+	ovpm.SetupDB("sqlite3", ":memory:")
+	defer ovpm.CeaseDB()
+	ovpm.Init("localhost", "")
+
+	// Prepare:
+	username := "testUser"
+	password := "testPasswd1234"
+	noGW := false
+
+	// Test:
+	user, err := ovpm.CreateNewUser(username, password, noGW, 0)
+	if err != nil {
+		t.Fatalf("user can not be created: %v", err)
+	}
+
+	var updatetests = []struct {
+		password string
+		noGW     bool
+		ok       bool
+	}{
+		{"testpw", false, true},
+		{"123", false, true},
+		{"123", false, true},
+		{"", true, true},
+	}
+
+	for _, tt := range updatetests {
+		err := user.Update(tt.password, tt.noGW, 0)
+		if (err == nil) != tt.ok {
+			t.Errorf("user is expected to be able to update but it gave us this error instead: %v", err)
+		}
+	}
 }
 
 func TestUserPasswordCorrect(t *testing.T) {
@@ -74,10 +124,9 @@ func TestUserPasswordCorrect(t *testing.T) {
 
 	// Prepare:
 	initialPassword := "g00dp@ssW0rd9"
-	user, _ := ovpm.CreateNewUser("testUser", initialPassword)
+	user, _ := ovpm.CreateNewUser("testUser", initialPassword, false, 0)
 
 	// Test:
-
 	// Is user created with the correct password?
 	if !user.CheckPassword(initialPassword) {
 		t.Fatalf("user's password must be '%s', but CheckPassword fails +%v", initialPassword, user)
@@ -93,7 +142,7 @@ func TestUserPasswordReset(t *testing.T) {
 
 	// Prepare:
 	initialPassword := "g00dp@ssW0rd9"
-	user, _ := ovpm.CreateNewUser("testUser", initialPassword)
+	user, _ := ovpm.CreateNewUser("testUser", initialPassword, false, 0)
 
 	// Test:
 
@@ -121,7 +170,7 @@ func TestUserDelete(t *testing.T) {
 
 	// Prepare:
 	username := "testUser"
-	user, _ := ovpm.CreateNewUser(username, "1234")
+	user, _ := ovpm.CreateNewUser(username, "1234", false, 0)
 
 	// Test:
 
@@ -160,7 +209,7 @@ func TestUserGet(t *testing.T) {
 
 	// Prepare:
 	username := "testUser"
-	user, _ := ovpm.CreateNewUser(username, "1234")
+	user, _ := ovpm.CreateNewUser(username, "1234", false, 0)
 
 	// Test:
 	// Is user fetchable?
@@ -189,7 +238,7 @@ func TestUserGetAll(t *testing.T) {
 	for i := 0; i < count; i++ {
 		username := fmt.Sprintf("user%d", i)
 		password := fmt.Sprintf("password%d", i)
-		user, _ := ovpm.CreateNewUser(username, password)
+		user, _ := ovpm.CreateNewUser(username, password, false, 0)
 		users = append(users, user)
 	}
 
@@ -223,7 +272,7 @@ func TestUserRenew(t *testing.T) {
 	ovpm.Init("localhost", "")
 
 	// Prepare:
-	user, _ := ovpm.CreateNewUser("user", "1234")
+	user, _ := ovpm.CreateNewUser("user", "1234", false, 0)
 
 	// Test:
 	// Re initialize the server.
@@ -238,6 +287,44 @@ func TestUserRenew(t *testing.T) {
 	}
 }
 
+func TestUserIPAllocator(t *testing.T) {
+	// Initialize:
+	ovpm.Testing = true
+	ovpm.SetupDB("sqlite3", ":memory:")
+	defer ovpm.CeaseDB()
+	ovpm.Init("localhost", "")
+
+	// Prepare:
+
+	// Test:
+	var iptests = []struct {
+		username   string
+		gw         bool
+		hostid     uint32
+		expectedIP string
+		pass       bool
+	}{
+		{"user1", false, 0, "10.9.0.2/24", true},
+		{"user2", false, 0, "10.9.0.3/24", true},
+		{"user3", true, 0, "10.9.0.4/24", true},
+		{"user4", true, ovpm.IP2HostID(net.ParseIP("10.9.0.5").To4()), "10.9.0.5/24", true},
+		{"user5", true, ovpm.IP2HostID(net.ParseIP("192.168.1.1").To4()), "10.9.0.6/24", false},
+		{"user6", true, ovpm.IP2HostID(net.ParseIP("10.9.0.7").To4()), "10.9.0.7/24", true},
+		{"user7", true, 0, "10.9.0.8/24", true},
+	}
+	for _, tt := range iptests {
+		user, err := ovpm.CreateNewUser(tt.username, "pass", tt.gw, tt.hostid)
+		if (err == nil) == !tt.pass {
+			t.Fatalf("expected pass %t %s", tt.pass, err)
+		}
+		if user != nil {
+			if user.GetIPNet() != tt.expectedIP {
+				t.Fatalf("%s is expected to be %s", user.GetIPNet(), tt.expectedIP)
+			}
+		}
+	}
+}
+
 // areUsersEqual compares given users and returns true if they are the same.
 func areUsersEqual(user1, user2 *ovpm.DBUser) bool {
 	if user1.Cert != user2.Cert {
@@ -248,8 +335,8 @@ func areUsersEqual(user1, user2 *ovpm.DBUser) bool {
 		logrus.Infof("Username %v != %v", user1.Username, user2.Username)
 		return false
 	}
-	if user1.Password != user2.Password {
-		logrus.Infof("Password %v != %v", user1.Password, user2.Password)
+	if user1.Hash != user2.Hash {
+		logrus.Infof("Password %v != %v", user1.Hash, user2.Hash)
 		return false
 	}
 	if user1.ServerSerialNumber != user2.ServerSerialNumber {

+ 40 - 16
vpn.go

@@ -1,4 +1,5 @@
 //go:generate go-bindata -pkg bindata -o bindata/bindata.go template/
+//go:generate protoc -I pb/ pb/user.proto pb/vpn.proto --go_out=plugins=grpc:pb
 
 package ovpm
 
@@ -6,12 +7,13 @@ import (
 	"bytes"
 	"fmt"
 	"math/big"
-	"net"
 	"os"
 	"os/exec"
 	"strings"
 	"text/template"
 
+	"time"
+
 	"github.com/Sirupsen/logrus"
 	"github.com/asaskevich/govalidator"
 	"github.com/cad/ovpm/bindata"
@@ -73,7 +75,7 @@ func Init(hostname string, port string) error {
 	}
 
 	if !govalidator.IsNumeric(port) {
-		return fmt.Errorf("validation error: port:`%s` should be numeric", hostname)
+		return fmt.Errorf("validation error: port:`%s` should be numeric", port)
 	}
 
 	serverName := "default"
@@ -131,6 +133,7 @@ func Init(hostname string, port string) error {
 			continue
 		}
 	}
+	Emit()
 	logrus.Infof("server initialized")
 	return nil
 }
@@ -143,6 +146,7 @@ func Deinit() error {
 
 	db.Unscoped().Delete(&DBServer{})
 	db.Unscoped().Delete(&DBRevoked{})
+	Emit()
 	return nil
 }
 
@@ -165,12 +169,14 @@ func DumpsClientConfig(username string) (string, error) {
 		CA       string
 		Key      string
 		Cert     string
+		NoGW     bool
 	}{
 		Hostname: server.Hostname,
 		Port:     server.Port,
 		CA:       server.CACert,
 		Key:      user.Key,
 		Cert:     user.Cert,
+		NoGW:     user.NoGW,
 	}
 	data, err := bindata.Asset("template/client.ovpn.tmpl")
 	if err != nil {
@@ -233,8 +239,8 @@ func StartVPNProc() {
 		logrus.Error("OpenVPN is already started")
 		return
 	}
-
 	vpnProc.Start()
+	ensureNatEnabled()
 }
 
 // RestartVPNProc restarts the OpenVPN process.
@@ -320,10 +326,15 @@ func Emit() error {
 
 	logrus.Info("configurations emitted to the filesystem")
 
-	// If the OpenVPN is already running, restart it.
-	if vpnProc.Status() == supervisor.RUNNING {
-		logrus.Info("OpenVPN process is restarting")
-		RestartVPNProc()
+	if IsInitialized() {
+		for {
+			if vpnProc.Status() == supervisor.RUNNING || vpnProc.Status() == supervisor.STOPPED {
+				logrus.Info("OpenVPN process is restarting")
+				RestartVPNProc()
+				break
+			}
+			time.Sleep(1 * time.Second)
+		}
 	}
 
 	return nil
@@ -476,21 +487,32 @@ func emitCCD() error {
 	if err != nil {
 		return err
 	}
+	if !Testing {
+		// Clean and then create and write rendered ccd data.
+		err = os.RemoveAll(_DefaultVPNCCDPath)
+		if err != nil {
+			if os.IsNotExist(err) {
+			} else {
+				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()
+		if _, err := os.Stat(_DefaultVPNCCDPath); err != nil {
+		}
 
-	counter := 2
+		err = os.Mkdir(_DefaultVPNCCDPath, 0755)
+		if err != nil {
+			if !os.IsExist(err) {
+				return err
+			}
+		}
+	}
 	for _, user := range users {
 		var result bytes.Buffer
-		clientNet[3] = byte(counter)
 		params := struct {
 			IP      string
 			NetMask string
-		}{IP: clientNet.String(), NetMask: _DefaultServerNetMask}
+		}{IP: user.getIP().String(), NetMask: _DefaultServerNetMask}
 
 		data, err := bindata.Asset("template/ccd.file.tmpl")
 		if err != nil {
@@ -510,7 +532,6 @@ func emitCCD() error {
 		if err != nil {
 			return err
 		}
-		counter++
 	}
 	return nil
 }
@@ -587,6 +608,9 @@ func checkIptablesExecutable() bool {
 }
 
 func ensureBaseDir() {
+	if Testing {
+		return
+	}
 	os.Mkdir(varBasePath, 0755)
 }
 

+ 90 - 20
vpn_test.go

@@ -1,17 +1,25 @@
 package ovpm
 
 import (
+	"strings"
 	"testing"
 
+	"github.com/Sirupsen/logrus"
 	"github.com/bouk/monkey"
 	"github.com/cad/ovpm/supervisor"
 )
 
 var fs map[string]string
 
+func setupTestCase() {
+	// Initialize.
+	fs = make(map[string]string)
+	vpnProc.Stop()
+}
+
 func TestVPNInit(t *testing.T) {
 	// Init:
-	Testing = true
+	setupTestCase()
 	SetupDB("sqlite3", ":memory:")
 	defer CeaseDB()
 	// Prepare:
@@ -26,6 +34,12 @@ func TestVPNInit(t *testing.T) {
 		t.Fatalf("server is expected to be empty struct(new record) but it isn't %+v", server)
 	}
 
+	// Wrongfully initialize server.
+	err := Init("localhost", "asdf")
+	if err == nil {
+		t.Fatalf("error is expected to be not nil but it's nil instead")
+	}
+
 	// Initialize the server.
 	Init("localhost", "")
 
@@ -41,14 +55,17 @@ func TestVPNInit(t *testing.T) {
 
 func TestVPNDeinit(t *testing.T) {
 	// Init:
-	Testing = true
+	setupTestCase()
 	SetupDB("sqlite3", ":memory:")
 	defer CeaseDB()
 
 	// Prepare:
 	// Initialize the server.
 	Init("localhost", "")
-	u, _ := CreateNewUser("user", "p")
+	u, err := CreateNewUser("user", "p", false, 0)
+	if err != nil {
+		t.Fatal(err)
+	}
 	u.Delete()
 
 	// Test:
@@ -92,7 +109,7 @@ func TestVPNDeinit(t *testing.T) {
 
 func TestVPNIsInitialized(t *testing.T) {
 	// Init:
-	Testing = true
+	setupTestCase()
 	SetupDB("sqlite3", ":memory:")
 	defer CeaseDB()
 
@@ -115,7 +132,7 @@ func TestVPNIsInitialized(t *testing.T) {
 
 func TestVPNGetServerInstance(t *testing.T) {
 	// Init:
-	Testing = true
+	setupTestCase()
 	SetupDB("sqlite3", ":memory:")
 	defer CeaseDB()
 
@@ -152,13 +169,13 @@ func TestVPNGetServerInstance(t *testing.T) {
 
 func TestVPNDumpsClientConfig(t *testing.T) {
 	// Init:
-	Testing = true
+	setupTestCase()
 	SetupDB("sqlite3", ":memory:")
 	defer CeaseDB()
 	Init("localhost", "")
 
 	// Prepare:
-	user, _ := CreateNewUser("user", "password")
+	user, _ := CreateNewUser("user", "password", false, 0)
 
 	// Test:
 	clientConfigBlob, err := DumpsClientConfig(user.GetUsername())
@@ -174,16 +191,20 @@ func TestVPNDumpsClientConfig(t *testing.T) {
 
 func TestVPNDumpClientConfig(t *testing.T) {
 	// Init:
-	Testing = true
+	setupTestCase()
 	SetupDB("sqlite3", ":memory:")
 	defer CeaseDB()
 	Init("localhost", "")
 
 	// Prepare:
-	user, _ := CreateNewUser("user", "password")
+	noGW := false
+	user, err := CreateNewUser("user", "password", noGW, 0)
+	if err != nil {
+		t.Fatalf("can not create user: %v", err)
+	}
 
 	// Test:
-	err := DumpClientConfig(user.GetUsername(), "/tmp/user.ovpn")
+	err = DumpClientConfig(user.GetUsername(), "/tmp/user.ovpn")
 	if err != nil {
 		t.Fatalf("expected to dump client config but we got error instead: %v", err)
 	}
@@ -195,11 +216,40 @@ func TestVPNDumpClientConfig(t *testing.T) {
 	if len(clientConfigBlob) == 0 {
 		t.Fatal("expected the dump not empty but it's empty instead")
 	}
+
+	// Is noGW honored?
+	if strings.Contains(clientConfigBlob, "route-nopull") != noGW {
+		logrus.Info(clientConfigBlob)
+		t.Fatalf("client config generator doesn't honor NoGW")
+	}
+
+	user.Delete()
+
+	noGW = true
+	user, err = CreateNewUser("user", "password", noGW, 0)
+	if err != nil {
+		t.Fatalf("can not create user: %v", err)
+	}
+
+	err = DumpClientConfig(user.GetUsername(), "/tmp/user.ovpn")
+	if err != nil {
+		t.Fatalf("expected to dump client config but we got error instead: %v", err)
+	}
+
+	// Read file.
+	clientConfigBlob = fs["/tmp/user.ovpn"]
+
+	// Is noGW honored?
+	if strings.Contains(clientConfigBlob, "route-nopull") != noGW {
+		logrus.Info(clientConfigBlob)
+		t.Fatalf("client config generator doesn't honor NoGW")
+	}
+
 }
 
 func TestVPNGetSystemCA(t *testing.T) {
 	// Init:
-	Testing = true
+	setupTestCase()
 	SetupDB("sqlite3", ":memory:")
 	defer CeaseDB()
 
@@ -231,7 +281,7 @@ func TestVPNGetSystemCA(t *testing.T) {
 
 func TestVPNStartVPNProc(t *testing.T) {
 	// Init:
-	Testing = true
+	setupTestCase()
 	SetupDB("sqlite3", ":memory:")
 	defer CeaseDB()
 
@@ -265,7 +315,7 @@ func TestVPNStartVPNProc(t *testing.T) {
 
 func TestVPNStopVPNProc(t *testing.T) {
 	// Init:
-	Testing = true
+	setupTestCase()
 	SetupDB("sqlite3", ":memory:")
 	defer CeaseDB()
 	Init("localhost", "")
@@ -300,10 +350,6 @@ func TestVPNRestartVPNProc(t *testing.T) {
 	// Test:
 
 	// Call restart.
-	// Isn't it stopped?
-	if vpnProc.Status() != supervisor.STOPPED {
-		t.Fatalf("expected state is STOPPED, got %s instead", vpnProc.Status())
-	}
 
 	RestartVPNProc()
 
@@ -323,7 +369,7 @@ func TestVPNRestartVPNProc(t *testing.T) {
 
 func TestVPNEmit(t *testing.T) {
 	// Init:
-	Testing = true
+	setupTestCase()
 	SetupDB("sqlite3", ":memory:")
 	defer CeaseDB()
 	Init("localhost", "")
@@ -352,6 +398,30 @@ func TestVPNEmit(t *testing.T) {
 	// TODO(cad): Write test cases for ccd/ files as well.
 }
 
+func TestVPNemitToFile(t *testing.T) {
+	// Initialize:
+	// Prepare:
+	path := "/test/file"
+	content := "blah blah blah"
+
+	// Test:
+	// Is path exist?
+	if _, ok := fs[path]; ok {
+		t.Fatalf("key '%s' expected to be non-existent on fs, but it is instead", path)
+	}
+
+	// Emit the contents.
+	err := emitToFile(path, content, 0)
+	if err != nil {
+		t.Fatalf("expected  to be able to emit to the filesystem but we got this error instead: %v", err)
+	}
+
+	// Is the content on the filesystem correct?
+	if fs[path] != content {
+		t.Fatalf("content on the filesytem is expected to be same with '%s' but it's '%s' instead", content, fs[path])
+	}
+}
+
 type fakeProcess struct {
 	state supervisor.State
 }
@@ -373,9 +443,9 @@ func (f *fakeProcess) Status() supervisor.State {
 }
 
 func init() {
-	// Initialize.
+	// Init
+	Testing = true
 	fs = make(map[string]string)
-
 	// Monkeypatch emitToFile()
 	monkey.Patch(emitToFile, func(path, content string, mode uint) error {
 		fs[path] = content