Jelajahi Sumber

Merge branch 'feat/networks' into dev

Mustafa Arici 8 tahun lalu
induk
melakukan
b12cc36fcd
14 mengubah file dengan 1263 tambahan dan 418 penghapusan
  1. 60 0
      api/rpc.go
  2. 5 5
      bindata/bindata.go
  3. 17 401
      cmd/ovpm/main.go
  4. 118 0
      cmd/ovpm/net.go
  5. 326 0
      cmd/ovpm/user.go
  6. 0 1
      cmd/ovpm/utils.go
  7. 105 0
      cmd/ovpm/vpn.go
  8. 1 0
      cmd/ovpmd/main.go
  9. 108 0
      net.go
  10. 163 0
      net_test.go
  11. 318 0
      pb/network.pb.go
  12. 33 0
      pb/network.proto
  13. 8 0
      pb/user.pb.go
  14. 1 11
      vpn.go

+ 60 - 0
api/rpc.go

@@ -177,3 +177,63 @@ func (s *VPNService) Init(ctx context.Context, req *pb.VPNInitRequest) (*pb.VPNI
 	}
 	return &pb.VPNInitResponse{}, nil
 }
+
+type NetworkService struct{}
+
+func (s *NetworkService) List(ctx context.Context, req *pb.NetworkListRequest) (*pb.NetworkListResponse, error) {
+	logrus.Debug("rpc call: network list")
+	var nt []*pb.Network
+
+	networks, err := ovpm.GetAllNetworks()
+	if err != nil {
+		logrus.Errorf("networks can not be fetched: %v", err)
+		os.Exit(1)
+		return nil, err
+	}
+	for _, network := range networks {
+		nt = append(nt, &pb.Network{
+			Name:      network.GetName(),
+			CIDR:      network.GetCIDR(),
+			CreatedAt: network.GetCreatedAt(),
+		})
+	}
+
+	return &pb.NetworkListResponse{Networks: nt}, nil
+}
+
+func (s *NetworkService) Create(ctx context.Context, req *pb.NetworkCreateRequest) (*pb.NetworkCreateResponse, error) {
+	logrus.Debugf("rpc call: network create: %s", req.Name)
+	network, err := ovpm.CreateNewNetwork(req.Name, req.CIDR)
+	if err != nil {
+		return nil, err
+	}
+
+	n := pb.Network{
+		Name:      network.GetName(),
+		CIDR:      network.GetCIDR(),
+		CreatedAt: network.GetCreatedAt(),
+	}
+
+	return &pb.NetworkCreateResponse{Network: &n}, nil
+}
+
+func (s *NetworkService) Delete(ctx context.Context, req *pb.NetworkDeleteRequest) (*pb.NetworkDeleteResponse, error) {
+	logrus.Debugf("rpc call: network delete: %s", req.Name)
+	network, err := ovpm.GetNetwork(req.Name)
+	if err != nil {
+		return nil, err
+	}
+
+	err = network.Delete()
+	if err != nil {
+		return nil, err
+	}
+
+	n := pb.Network{
+		Name:      network.GetName(),
+		CIDR:      network.GetCIDR(),
+		CreatedAt: network.GetCreatedAt(),
+	}
+
+	return &pb.NetworkDeleteResponse{Network: &n}, nil
+}

+ 5 - 5
bindata/bindata.go

@@ -87,7 +87,7 @@ func templateCcdFileTmpl() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "template/ccd.file.tmpl", size: 74, mode: os.FileMode(420), modTime: time.Unix(1502998813, 0)}
+	info := bindataFileInfo{name: "template/ccd.file.tmpl", size: 74, mode: os.FileMode(420), modTime: time.Unix(1503322186, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -107,7 +107,7 @@ func templateClientOvpnTmpl() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "template/client.ovpn.tmpl", size: 365, mode: os.FileMode(420), modTime: time.Unix(1503002198, 0)}
+	info := bindataFileInfo{name: "template/client.ovpn.tmpl", size: 365, mode: os.FileMode(420), modTime: time.Unix(1503410118, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -127,7 +127,7 @@ func templateDh4096PemTmpl() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "template/dh4096.pem.tmpl", size: 1468, mode: os.FileMode(420), modTime: time.Unix(1502796579, 0)}
+	info := bindataFileInfo{name: "template/dh4096.pem.tmpl", size: 1468, mode: os.FileMode(420), modTime: time.Unix(1503322186, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -147,7 +147,7 @@ func templateIptablesTmpl() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "template/iptables.tmpl", size: 0, mode: os.FileMode(420), modTime: time.Unix(1502796579, 0)}
+	info := bindataFileInfo{name: "template/iptables.tmpl", size: 0, mode: os.FileMode(420), modTime: time.Unix(1503322186, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -167,7 +167,7 @@ func templateServerConfTmpl() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "template/server.conf.tmpl", size: 9585, mode: os.FileMode(420), modTime: time.Unix(1502796579, 0)}
+	info := bindataFileInfo{name: "template/server.conf.tmpl", size: 9585, mode: os.FileMode(420), modTime: time.Unix(1503322186, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }

+ 17 - 401
cmd/ovpm/main.go

@@ -1,15 +1,10 @@
 package main
 
 import (
-	"context"
-	"fmt"
-	"net"
 	"os"
 
 	"github.com/Sirupsen/logrus"
 	"github.com/cad/ovpm"
-	"github.com/cad/ovpm/pb"
-	"github.com/olekukonko/tablewriter"
 	"github.com/urfave/cli"
 )
 
@@ -43,408 +38,29 @@ func main() {
 			Name:  "user",
 			Usage: "User Operations",
 			Subcommands: []cli.Command{
-				{
-					Name:  "list",
-					Usage: "List VPN users.",
-					Action: func(c *cli.Context) error {
-						action = "user:list"
-						conn := getConn(c.GlobalString("daemon-port"))
-						defer conn.Close()
-						userSvc := pb.NewUserServiceClient(conn)
-						vpnSvc := pb.NewVPNServiceClient(conn)
-
-						server, err := vpnSvc.Status(context.Background(), &pb.VPNStatusRequest{})
-						if err != nil {
-							logrus.Errorf("can not get server status: %v", err)
-							os.Exit(1)
-							return err
-						}
-
-						resp, err := userSvc.List(context.Background(), &pb.UserListRequest{})
-						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", "ip", "created at", "valid crt", "no gw"})
-						//table.SetBorder(false)
-						for i, user := range resp.Users {
-							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()
-
-						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",
-						},
-						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, NoGW: noGW, HostID: hostid})
-						if err != nil {
-							logrus.Errorf("user can not be created '%s': %v", username, err)
-							os.Exit(1)
-							return err
-						}
-						logrus.Infof("user created: %s", response.Users[0].Username)
-						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.",
-					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)
-						}
-
-						//conn := getConn(c.String("port"))
-						conn := getConn(c.GlobalString("daemon-port"))
-						defer conn.Close()
-						userSvc := pb.NewUserServiceClient(conn)
-
-						_, err := userSvc.Delete(context.Background(), &pb.UserDeleteRequest{Username: 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)
-						}
-
-						//conn := getConn(c.String("port"))
-						conn := getConn(c.GlobalString("daemon-port"))
-						defer conn.Close()
-						userSvc := pb.NewUserServiceClient(conn)
-						pb.NewVPNServiceClient(conn)
-
-						_, err := userSvc.Renew(context.Background(), &pb.UserRenewRequest{Username: username})
-						if err != nil {
-							logrus.Errorf("can't renew user cert '%s': %v", username, err)
-							os.Exit(1)
-							return err
-						}
-						logrus.Infof("user cert 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"
-						}
-
-						//conn := getConn(c.String("port"))
-						conn := getConn(c.GlobalString("daemon-port"))
-						defer conn.Close()
-						userSvc := pb.NewUserServiceClient(conn)
-						pb.NewVPNServiceClient(conn)
-
-						res, err := userSvc.GenConfig(context.Background(), &pb.UserGenConfigRequest{Username: username})
-						if err != nil {
-							logrus.Errorf("user config can not be exported %s: %v", username, err)
-							return err
-						}
-						emitToFile(output, res.ClientConfig, 0)
-						logrus.Infof("exported to %s", output)
-						return nil
-					},
-				},
+				userListCommand,
+				userCreateCommand,
+				userUpdateCommand,
+				userDeleteCommand,
+				userRenewCommand,
+				userGenconfigCommand,
 			},
 		},
 		{
 			Name:  "vpn",
 			Usage: "VPN Operations",
 			Subcommands: []cli.Command{
-				{
-					Name:  "status",
-					Usage: "Show VPN status.",
-					Action: func(c *cli.Context) error {
-						conn := getConn(c.GlobalString("daemon-port"))
-						defer conn.Close()
-						vpnSvc := pb.NewVPNServiceClient(conn)
-
-						res, err := vpnSvc.Status(context.Background(), &pb.VPNStatusRequest{})
-						if err != nil {
-							os.Exit(1)
-							return err
-						}
-
-						table := tablewriter.NewWriter(os.Stdout)
-						table.SetHeader([]string{"attribute", "value"})
-						table.Append([]string{"Name", res.Name})
-						table.Append([]string{"Hostname", res.Hostname})
-						table.Append([]string{"Port", res.Port})
-						table.Append([]string{"Network", res.Net})
-						table.Append([]string{"Netmask", res.Mask})
-						table.Append([]string{"Created At", res.CreatedAt})
-						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"
-						hostname := c.String("hostname")
-						if hostname == "" {
-							logrus.Errorf("'hostname' is needed")
-							fmt.Println(cli.ShowSubcommandHelp(c))
-							os.Exit(1)
-
-						}
-
-						port := c.String("port")
-						if port == "" {
-							port = ovpm.DefaultVPNPort
-						}
-
-						conn := getConn(c.GlobalString("daemon-port"))
-						defer conn.Close()
-						vpnSvc := pb.NewVPNServiceClient(conn)
-
-						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 := vpnSvc.Init(context.Background(), &pb.VPNInitRequest{Hostname: hostname, Port: port}); err != nil {
-									logrus.Errorf("server can not be initialized: %v", err)
-									os.Exit(1)
-									return err
-								}
-								logrus.Info("ovpm server initialized")
-								break
-							} else if stringInSlice(response, nokayResponses) {
-								return fmt.Errorf("user decided to cancel")
-							}
-						}
-
-						return nil
-					},
-				},
+				vpnStatusCommand,
+				vpnInitCommand,
+			},
+		},
+		{
+			Name:  "net",
+			Usage: "Network Operations",
+			Subcommands: []cli.Command{
+				netListCommand,
+				netDefineCommand,
+				netUndefineCommand,
 			},
 		},
 	}

+ 118 - 0
cmd/ovpm/net.go

@@ -0,0 +1,118 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"os"
+
+	"github.com/Sirupsen/logrus"
+	"github.com/cad/ovpm/pb"
+	"github.com/olekukonko/tablewriter"
+	"github.com/urfave/cli"
+)
+
+var netDefineCommand = cli.Command{
+	Name:  "define",
+	Usage: "Define a network.",
+	Flags: []cli.Flag{
+		cli.StringFlag{
+			Name:  "cidr, c",
+			Usage: "CIDR of the network",
+		},
+		cli.StringFlag{
+			Name:  "name, n",
+			Usage: "name of the network",
+		},
+	},
+	Action: func(c *cli.Context) error {
+		action = "net:create"
+		name := c.String("name")
+		cidr := c.String("cidr")
+
+		if name == "" || cidr == "" {
+			fmt.Println(cli.ShowSubcommandHelp(c))
+			os.Exit(1)
+		}
+
+		conn := getConn(c.GlobalString("daemon-port"))
+		defer conn.Close()
+		netSvc := pb.NewNetworkServiceClient(conn)
+
+		response, err := netSvc.Create(context.Background(), &pb.NetworkCreateRequest{Name: name, CIDR: cidr})
+		if err != nil {
+			logrus.Errorf("network can not be created '%s': %v", name, err)
+			os.Exit(1)
+			return err
+		}
+		logrus.Infof("network created: %s (%s)", response.Network.Name, response.Network.CIDR)
+		return nil
+	},
+}
+
+var netListCommand = cli.Command{
+	Name:  "list",
+	Usage: "List network definitions.",
+	Flags: []cli.Flag{
+		cli.StringFlag{
+			Name:  "cidr, c",
+			Usage: "CIDR of the network",
+		},
+	},
+	Action: func(c *cli.Context) error {
+		action = "net:list"
+		conn := getConn(c.GlobalString("daemon-port"))
+		defer conn.Close()
+		netSvc := pb.NewNetworkServiceClient(conn)
+
+		resp, err := netSvc.List(context.Background(), &pb.NetworkListRequest{})
+		if err != nil {
+			logrus.Errorf("networks can not be fetched: %v", err)
+			os.Exit(1)
+			return err
+		}
+		table := tablewriter.NewWriter(os.Stdout)
+		table.SetHeader([]string{"#", "name", "cidr", "created at"})
+		//table.SetBorder(false)
+		for i, network := range resp.Networks {
+			data := []string{fmt.Sprintf("%v", i+1), network.Name, network.CIDR, network.CreatedAt}
+			table.Append(data)
+		}
+		table.Render()
+
+		return nil
+	},
+}
+
+var netUndefineCommand = cli.Command{
+	Name:  "undefine",
+	Usage: "Undefine an existing network.",
+	Flags: []cli.Flag{
+		cli.StringFlag{
+			Name:  "name, n",
+			Usage: "name of the network",
+		},
+	},
+	Action: func(c *cli.Context) error {
+		action = "net:delete"
+		name := c.String("name")
+
+		if name == "" {
+			fmt.Println(cli.ShowSubcommandHelp(c))
+			os.Exit(1)
+		}
+
+		conn := getConn(c.GlobalString("daemon-port"))
+		defer conn.Close()
+		netSvc := pb.NewNetworkServiceClient(conn)
+
+		resp, err := netSvc.Delete(context.Background(), &pb.NetworkDeleteRequest{Name: name})
+		if err != nil {
+			logrus.Errorf("networks can not be deleted: %v", err)
+			os.Exit(1)
+			return err
+		}
+		logrus.Infof("network deleted: %s (%s)", resp.Network.Name, resp.Network.CIDR)
+
+		return nil
+	},
+}

+ 326 - 0
cmd/ovpm/user.go

@@ -0,0 +1,326 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"net"
+	"os"
+
+	"github.com/Sirupsen/logrus"
+	"github.com/cad/ovpm"
+	"github.com/cad/ovpm/pb"
+	"github.com/olekukonko/tablewriter"
+	"github.com/urfave/cli"
+)
+
+var userGenconfigCommand = cli.Command{
+	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"
+		}
+
+		//conn := getConn(c.String("port"))
+		conn := getConn(c.GlobalString("daemon-port"))
+		defer conn.Close()
+		userSvc := pb.NewUserServiceClient(conn)
+		pb.NewVPNServiceClient(conn)
+
+		res, err := userSvc.GenConfig(context.Background(), &pb.UserGenConfigRequest{Username: username})
+		if err != nil {
+			logrus.Errorf("user config can not be exported %s: %v", username, err)
+			return err
+		}
+		emitToFile(output, res.ClientConfig, 0)
+		logrus.Infof("exported to %s", output)
+		return nil
+	},
+}
+
+var userRenewCommand = cli.Command{
+	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)
+		}
+
+		//conn := getConn(c.String("port"))
+		conn := getConn(c.GlobalString("daemon-port"))
+		defer conn.Close()
+		userSvc := pb.NewUserServiceClient(conn)
+		pb.NewVPNServiceClient(conn)
+
+		_, err := userSvc.Renew(context.Background(), &pb.UserRenewRequest{Username: username})
+		if err != nil {
+			logrus.Errorf("can't renew user cert '%s': %v", username, err)
+			os.Exit(1)
+			return err
+		}
+		logrus.Infof("user cert renewed: '%s'", username)
+		return nil
+	},
+}
+
+var userDeleteCommand = cli.Command{
+	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)
+		}
+
+		//conn := getConn(c.String("port"))
+		conn := getConn(c.GlobalString("daemon-port"))
+		defer conn.Close()
+		userSvc := pb.NewUserServiceClient(conn)
+
+		_, err := userSvc.Delete(context.Background(), &pb.UserDeleteRequest{Username: 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
+	},
+}
+
+var userUpdateCommand = cli.Command{
+	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
+	},
+}
+
+var userCreateCommand = cli.Command{
+	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",
+		},
+		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, NoGW: noGW, HostID: hostid})
+		if err != nil {
+			logrus.Errorf("user can not be created '%s': %v", username, err)
+			os.Exit(1)
+			return err
+		}
+		logrus.Infof("user created: %s", response.Users[0].Username)
+		return nil
+	},
+}
+
+var userListCommand = cli.Command{
+	Name:  "list",
+	Usage: "List VPN users.",
+	Action: func(c *cli.Context) error {
+		action = "user:list"
+		conn := getConn(c.GlobalString("daemon-port"))
+		defer conn.Close()
+		userSvc := pb.NewUserServiceClient(conn)
+		vpnSvc := pb.NewVPNServiceClient(conn)
+
+		server, err := vpnSvc.Status(context.Background(), &pb.VPNStatusRequest{})
+		if err != nil {
+			logrus.Errorf("can not get server status: %v", err)
+			os.Exit(1)
+			return err
+		}
+
+		resp, err := userSvc.List(context.Background(), &pb.UserListRequest{})
+		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", "ip", "created at", "valid crt", "no gw"})
+		//table.SetBorder(false)
+		for i, user := range resp.Users {
+			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()
+
+		return nil
+	},
+}

+ 0 - 1
cmd/ovpm/utils.go

@@ -33,5 +33,4 @@ func getConn(port string) *grpc.ClientConn {
 		logrus.Fatalf("fail to dial: %v", err)
 	}
 	return conn
-
 }

+ 105 - 0
cmd/ovpm/vpn.go

@@ -0,0 +1,105 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"os"
+
+	"github.com/Sirupsen/logrus"
+	"github.com/cad/ovpm"
+	"github.com/cad/ovpm/pb"
+	"github.com/olekukonko/tablewriter"
+	"github.com/urfave/cli"
+)
+
+var vpnStatusCommand = cli.Command{
+	Name:  "status",
+	Usage: "Show VPN status.",
+	Action: func(c *cli.Context) error {
+		conn := getConn(c.GlobalString("daemon-port"))
+		defer conn.Close()
+		vpnSvc := pb.NewVPNServiceClient(conn)
+
+		res, err := vpnSvc.Status(context.Background(), &pb.VPNStatusRequest{})
+		if err != nil {
+			os.Exit(1)
+			return err
+		}
+
+		table := tablewriter.NewWriter(os.Stdout)
+		table.SetHeader([]string{"attribute", "value"})
+		table.Append([]string{"Name", res.Name})
+		table.Append([]string{"Hostname", res.Hostname})
+		table.Append([]string{"Port", res.Port})
+		table.Append([]string{"Network", res.Net})
+		table.Append([]string{"Netmask", res.Mask})
+		table.Append([]string{"Created At", res.CreatedAt})
+		table.Render()
+
+		return nil
+	},
+}
+
+var vpnInitCommand = cli.Command{
+	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"
+		hostname := c.String("hostname")
+		if hostname == "" {
+			logrus.Errorf("'hostname' is needed")
+			fmt.Println(cli.ShowSubcommandHelp(c))
+			os.Exit(1)
+
+		}
+
+		port := c.String("port")
+		if port == "" {
+			port = ovpm.DefaultVPNPort
+		}
+
+		conn := getConn(c.GlobalString("daemon-port"))
+		defer conn.Close()
+		vpnSvc := pb.NewVPNServiceClient(conn)
+
+		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 := vpnSvc.Init(context.Background(), &pb.VPNInitRequest{Hostname: hostname, Port: port}); err != nil {
+					logrus.Errorf("server can not be initialized: %v", err)
+					os.Exit(1)
+					return err
+				}
+				logrus.Info("ovpm server initialized")
+				break
+			} else if stringInSlice(response, nokayResponses) {
+				return fmt.Errorf("user decided to cancel")
+			}
+		}
+
+		return nil
+	},
+}

+ 1 - 0
cmd/ovpmd/main.go

@@ -91,6 +91,7 @@ func newServer(port string) *server {
 	s := grpc.NewServer()
 	pb.RegisterUserServiceServer(s, &api.UserService{})
 	pb.RegisterVPNServiceServer(s, &api.VPNService{})
+	pb.RegisterNetworkServiceServer(s, &api.NetworkService{})
 	return &server{lis: lis, grpcServer: s, signal: sigs, done: done, port: port}
 }
 

+ 108 - 0
net.go

@@ -8,9 +8,115 @@ import (
 	"time"
 
 	"github.com/Sirupsen/logrus"
+	"github.com/asaskevich/govalidator"
 	"github.com/coreos/go-iptables/iptables"
+	"github.com/jinzhu/gorm"
 )
 
+// DBNetwork is database model for external networks on the VPN server.
+type DBNetwork struct {
+	gorm.Model
+	ServerID uint
+	Server   DBServer
+
+	Name string `gorm:"unique_index"`
+	CIDR string
+}
+
+// GetNetwork returns a network specified by its name.
+func GetNetwork(name string) (*DBNetwork, error) {
+	if !IsInitialized() {
+		return nil, fmt.Errorf("you first need to create server")
+	}
+	// Validate user input.
+	if govalidator.IsNull(name) {
+		return nil, fmt.Errorf("validation error: %s can not be null", name)
+	}
+	if !govalidator.IsAlphanumeric(name) {
+		return nil, fmt.Errorf("validation error: `%s` can only contain letters and numbers", name)
+	}
+
+	var network DBNetwork
+	db.Where(&DBNetwork{Name: name}).First(&network)
+
+	if db.NewRecord(&network) {
+		return nil, fmt.Errorf("network not found %s", name)
+	}
+
+	return &network, nil
+}
+
+// GetAllNetworks returns all networks defined in the system.
+func GetAllNetworks() ([]*DBNetwork, error) {
+	var networks []*DBNetwork
+	db.Find(&networks)
+
+	return networks, nil
+}
+
+// CreateNewNetwork creates a new network definition in the system.
+func CreateNewNetwork(name, cidr string) (*DBNetwork, error) {
+	if !IsInitialized() {
+		return nil, fmt.Errorf("you first need to create server")
+	}
+	// Validate user input.
+	if govalidator.IsNull(name) {
+		return nil, fmt.Errorf("validation error: %s can not be null", name)
+	}
+	if !govalidator.IsAlphanumeric(name) {
+		return nil, fmt.Errorf("validation error: `%s` can only contain letters and numbers", name)
+	}
+
+	if !govalidator.IsCIDR(cidr) {
+		return nil, fmt.Errorf("validation error: `%s` must be a network in the CIDR form", name)
+	}
+
+	_, ipnet, err := net.ParseCIDR(cidr)
+	if err != nil {
+		return nil, fmt.Errorf("can not parse CIDR %s: %v", cidr, err)
+	}
+
+	network := DBNetwork{
+		Name: name,
+		CIDR: ipnet.String(),
+	}
+	db.Save(&network)
+
+	if db.NewRecord(&network) {
+		return nil, fmt.Errorf("can not create network in the db")
+	}
+
+	return &network, nil
+
+}
+
+// Delete deletes a network definition in the system.
+func (n *DBNetwork) Delete() error {
+	if !IsInitialized() {
+		return fmt.Errorf("you first need to create server")
+	}
+
+	db.Unscoped().Delete(n)
+	logrus.Infof("network deleted: %s", n.Name)
+
+	return nil
+}
+
+// GetName returns network's name.
+func (n *DBNetwork) GetName() string {
+	return n.Name
+}
+
+// GetCIDR returns network's CIDR.
+func (n *DBNetwork) GetCIDR() string {
+	return n.CIDR
+}
+
+// GetCreatedAt returns network's name.
+func (n *DBNetwork) GetCreatedAt() string {
+	return n.CreatedAt.Format(time.UnixDate)
+}
+
 // routedInterface returns a network interface that can route IP
 // traffic and satisfies flags. It returns nil when an appropriate
 // network interface is not found. Network must be "ip", "ip4" or
@@ -161,12 +267,14 @@ func enableNat() error {
 
 }
 
+// HostID2IP converts a host id (32-bit unsigned integer) to an IP address.
 func HostID2IP(hostid uint32) net.IP {
 	ip := make([]byte, 4)
 	binary.BigEndian.PutUint32(ip, hostid)
 	return net.IP(ip)
 }
 
+//IP2HostID converts an IP address to a host id (32-bit unsigned integer).
 func IP2HostID(ip net.IP) uint32 {
 	hostid := binary.BigEndian.Uint32(ip)
 	return hostid

+ 163 - 0
net_test.go

@@ -0,0 +1,163 @@
+package ovpm
+
+import "testing"
+
+func TestVPNCreateNewNetwork(t *testing.T) {
+	// Initialize:
+	setupTestCase()
+	SetupDB("sqlite3", ":memory:")
+	defer CeaseDB()
+	Init("localhost", "")
+
+	// Prepare:
+	// Test:
+	netName := "testnet"
+	cidrStr := "192.168.1.0/24"
+
+	n, err := CreateNewNetwork(netName, cidrStr)
+	if err != nil {
+		t.Fatalf("unexpected error when creating a new network: %v", err)
+	}
+
+	if n.Name != netName {
+		t.Fatalf("network Name is expected to be '%s' but it's '%s' instead", netName, n.Name)
+	}
+
+	if n.CIDR != cidrStr {
+		t.Fatalf("network CIDR is expected to be '%s' but it's '%s' instead", cidrStr, n.CIDR)
+	}
+
+	var network DBNetwork
+	db.First(&network)
+
+	if db.NewRecord(&network) {
+		t.Fatalf("network is not created in the database.")
+	}
+
+	if network.Name != netName {
+		t.Fatalf("network Name is expected to be '%s' but it's '%s' instead", netName, network.Name)
+	}
+
+	if network.CIDR != cidrStr {
+		t.Fatalf("network CIDR is expected to be '%s' but it's '%s' instead", cidrStr, network.CIDR)
+	}
+
+}
+
+func TestVPNDeleteNetwork(t *testing.T) {
+	// Initialize:
+	setupTestCase()
+	SetupDB("sqlite3", ":memory:")
+	defer CeaseDB()
+	Init("localhost", "")
+
+	// Prepare:
+	// Test:
+	netName := "testnet"
+	cidrStr := "192.168.1.0/24"
+
+	n, err := CreateNewNetwork(netName, cidrStr)
+	if err != nil {
+		t.Fatalf("unexpected error when creating a new network: %v", err)
+	}
+
+	var network DBNetwork
+	db.First(&network)
+
+	if db.NewRecord(&network) {
+		t.Fatalf("network is not created in the database.")
+	}
+
+	err = n.Delete()
+	if err != nil {
+		t.Fatalf("can't delete network: %v", err)
+	}
+
+	// Empty the existing network object.
+	network = DBNetwork{}
+	db.First(&network)
+	if !db.NewRecord(&network) {
+		t.Fatalf("network is not deleted from the database. %+v", network)
+	}
+}
+
+func TestVPNGetNetwork(t *testing.T) {
+	// Initialize:
+	setupTestCase()
+	SetupDB("sqlite3", ":memory:")
+	defer CeaseDB()
+	Init("localhost", "")
+
+	// Prepare:
+	// Test:
+	netName := "testnet"
+	cidrStr := "192.168.1.0/24"
+
+	_, err := CreateNewNetwork(netName, cidrStr)
+	if err != nil {
+		t.Fatalf("unexpected error when creating a new network: %v", err)
+	}
+
+	var network DBNetwork
+	db.First(&network)
+
+	if db.NewRecord(&network) {
+		t.Fatalf("network is not created in the database.")
+	}
+
+	n, err := GetNetwork(netName)
+	if err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+
+	if db.NewRecord(&n) {
+		t.Fatalf("network is not correctly returned from db.")
+	}
+}
+
+func TestVPNGetAllNetworks(t *testing.T) {
+	// Initialize:
+	setupTestCase()
+	SetupDB("sqlite3", ":memory:")
+	defer CeaseDB()
+	Init("localhost", "")
+
+	// Prepare:
+	// Test:
+	var getallnettests = []struct {
+		name    string
+		cidr    string
+		passing bool
+	}{
+		{"testnet1", "192.168.1.0/24", true},
+		{"testnet2", "10.10.0.0/16", true},
+		{"testnet3", "asdkfjadflsa", false},
+	}
+	for _, tt := range getallnettests {
+		_, err := CreateNewNetwork(tt.name, tt.cidr)
+		if (err == nil) != tt.passing {
+			t.Fatalf("unexpected error when creating a new network: %v", err)
+		}
+	}
+
+	for _, tt := range getallnettests {
+		n, err := GetNetwork(tt.name)
+		if (err == nil) != tt.passing {
+			t.Fatalf("network's presence is expected to be '%t' but it's '%t' instead", tt.passing, !tt.passing)
+		}
+
+		if tt.passing {
+			if n.Name != tt.name {
+				t.Fatalf("network Name is expected to be '%s' but it's '%s'", tt.name, n.Name)
+			}
+			if n.CIDR != tt.cidr {
+				t.Fatalf("network CIDR is expected to be '%s' but it's '%s'", tt.cidr, n.CIDR)
+			}
+		}
+	}
+}
+
+func init() {
+	// Init
+	Testing = true
+}

+ 318 - 0
pb/network.pb.go

@@ -0,0 +1,318 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// source: network.proto
+
+package pb
+
+import proto "github.com/golang/protobuf/proto"
+import fmt "fmt"
+import math "math"
+
+import (
+	context "golang.org/x/net/context"
+	grpc "google.golang.org/grpc"
+)
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ = proto.Marshal
+var _ = fmt.Errorf
+var _ = math.Inf
+
+type NetworkCreateRequest struct {
+	Name string `protobuf:"bytes,1,opt,name=Name" json:"Name,omitempty"`
+	CIDR string `protobuf:"bytes,2,opt,name=CIDR" json:"CIDR,omitempty"`
+}
+
+func (m *NetworkCreateRequest) Reset()                    { *m = NetworkCreateRequest{} }
+func (m *NetworkCreateRequest) String() string            { return proto.CompactTextString(m) }
+func (*NetworkCreateRequest) ProtoMessage()               {}
+func (*NetworkCreateRequest) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{0} }
+
+func (m *NetworkCreateRequest) GetName() string {
+	if m != nil {
+		return m.Name
+	}
+	return ""
+}
+
+func (m *NetworkCreateRequest) GetCIDR() string {
+	if m != nil {
+		return m.CIDR
+	}
+	return ""
+}
+
+type NetworkListRequest struct {
+}
+
+func (m *NetworkListRequest) Reset()                    { *m = NetworkListRequest{} }
+func (m *NetworkListRequest) String() string            { return proto.CompactTextString(m) }
+func (*NetworkListRequest) ProtoMessage()               {}
+func (*NetworkListRequest) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{1} }
+
+type NetworkDeleteRequest struct {
+	Name string `protobuf:"bytes,1,opt,name=Name" json:"Name,omitempty"`
+}
+
+func (m *NetworkDeleteRequest) Reset()                    { *m = NetworkDeleteRequest{} }
+func (m *NetworkDeleteRequest) String() string            { return proto.CompactTextString(m) }
+func (*NetworkDeleteRequest) ProtoMessage()               {}
+func (*NetworkDeleteRequest) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{2} }
+
+func (m *NetworkDeleteRequest) GetName() string {
+	if m != nil {
+		return m.Name
+	}
+	return ""
+}
+
+type Network struct {
+	Name      string `protobuf:"bytes,1,opt,name=Name" json:"Name,omitempty"`
+	CIDR      string `protobuf:"bytes,2,opt,name=CIDR" json:"CIDR,omitempty"`
+	CreatedAt string `protobuf:"bytes,3,opt,name=CreatedAt" json:"CreatedAt,omitempty"`
+}
+
+func (m *Network) Reset()                    { *m = Network{} }
+func (m *Network) String() string            { return proto.CompactTextString(m) }
+func (*Network) ProtoMessage()               {}
+func (*Network) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{3} }
+
+func (m *Network) GetName() string {
+	if m != nil {
+		return m.Name
+	}
+	return ""
+}
+
+func (m *Network) GetCIDR() string {
+	if m != nil {
+		return m.CIDR
+	}
+	return ""
+}
+
+func (m *Network) GetCreatedAt() string {
+	if m != nil {
+		return m.CreatedAt
+	}
+	return ""
+}
+
+type NetworkCreateResponse struct {
+	Network *Network `protobuf:"bytes,1,opt,name=Network" json:"Network,omitempty"`
+}
+
+func (m *NetworkCreateResponse) Reset()                    { *m = NetworkCreateResponse{} }
+func (m *NetworkCreateResponse) String() string            { return proto.CompactTextString(m) }
+func (*NetworkCreateResponse) ProtoMessage()               {}
+func (*NetworkCreateResponse) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{4} }
+
+func (m *NetworkCreateResponse) GetNetwork() *Network {
+	if m != nil {
+		return m.Network
+	}
+	return nil
+}
+
+type NetworkListResponse struct {
+	Networks []*Network `protobuf:"bytes,1,rep,name=Networks" json:"Networks,omitempty"`
+}
+
+func (m *NetworkListResponse) Reset()                    { *m = NetworkListResponse{} }
+func (m *NetworkListResponse) String() string            { return proto.CompactTextString(m) }
+func (*NetworkListResponse) ProtoMessage()               {}
+func (*NetworkListResponse) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{5} }
+
+func (m *NetworkListResponse) GetNetworks() []*Network {
+	if m != nil {
+		return m.Networks
+	}
+	return nil
+}
+
+type NetworkDeleteResponse struct {
+	Network *Network `protobuf:"bytes,1,opt,name=Network" json:"Network,omitempty"`
+}
+
+func (m *NetworkDeleteResponse) Reset()                    { *m = NetworkDeleteResponse{} }
+func (m *NetworkDeleteResponse) String() string            { return proto.CompactTextString(m) }
+func (*NetworkDeleteResponse) ProtoMessage()               {}
+func (*NetworkDeleteResponse) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{6} }
+
+func (m *NetworkDeleteResponse) GetNetwork() *Network {
+	if m != nil {
+		return m.Network
+	}
+	return nil
+}
+
+func init() {
+	proto.RegisterType((*NetworkCreateRequest)(nil), "pb.NetworkCreateRequest")
+	proto.RegisterType((*NetworkListRequest)(nil), "pb.NetworkListRequest")
+	proto.RegisterType((*NetworkDeleteRequest)(nil), "pb.NetworkDeleteRequest")
+	proto.RegisterType((*Network)(nil), "pb.Network")
+	proto.RegisterType((*NetworkCreateResponse)(nil), "pb.NetworkCreateResponse")
+	proto.RegisterType((*NetworkListResponse)(nil), "pb.NetworkListResponse")
+	proto.RegisterType((*NetworkDeleteResponse)(nil), "pb.NetworkDeleteResponse")
+}
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ context.Context
+var _ grpc.ClientConn
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+const _ = grpc.SupportPackageIsVersion4
+
+// Client API for NetworkService service
+
+type NetworkServiceClient interface {
+	Create(ctx context.Context, in *NetworkCreateRequest, opts ...grpc.CallOption) (*NetworkCreateResponse, error)
+	List(ctx context.Context, in *NetworkListRequest, opts ...grpc.CallOption) (*NetworkListResponse, error)
+	Delete(ctx context.Context, in *NetworkDeleteRequest, opts ...grpc.CallOption) (*NetworkDeleteResponse, error)
+}
+
+type networkServiceClient struct {
+	cc *grpc.ClientConn
+}
+
+func NewNetworkServiceClient(cc *grpc.ClientConn) NetworkServiceClient {
+	return &networkServiceClient{cc}
+}
+
+func (c *networkServiceClient) Create(ctx context.Context, in *NetworkCreateRequest, opts ...grpc.CallOption) (*NetworkCreateResponse, error) {
+	out := new(NetworkCreateResponse)
+	err := grpc.Invoke(ctx, "/pb.NetworkService/Create", in, out, c.cc, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *networkServiceClient) List(ctx context.Context, in *NetworkListRequest, opts ...grpc.CallOption) (*NetworkListResponse, error) {
+	out := new(NetworkListResponse)
+	err := grpc.Invoke(ctx, "/pb.NetworkService/List", in, out, c.cc, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *networkServiceClient) Delete(ctx context.Context, in *NetworkDeleteRequest, opts ...grpc.CallOption) (*NetworkDeleteResponse, error) {
+	out := new(NetworkDeleteResponse)
+	err := grpc.Invoke(ctx, "/pb.NetworkService/Delete", in, out, c.cc, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+// Server API for NetworkService service
+
+type NetworkServiceServer interface {
+	Create(context.Context, *NetworkCreateRequest) (*NetworkCreateResponse, error)
+	List(context.Context, *NetworkListRequest) (*NetworkListResponse, error)
+	Delete(context.Context, *NetworkDeleteRequest) (*NetworkDeleteResponse, error)
+}
+
+func RegisterNetworkServiceServer(s *grpc.Server, srv NetworkServiceServer) {
+	s.RegisterService(&_NetworkService_serviceDesc, srv)
+}
+
+func _NetworkService_Create_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(NetworkCreateRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(NetworkServiceServer).Create(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/pb.NetworkService/Create",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(NetworkServiceServer).Create(ctx, req.(*NetworkCreateRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _NetworkService_List_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(NetworkListRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(NetworkServiceServer).List(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/pb.NetworkService/List",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(NetworkServiceServer).List(ctx, req.(*NetworkListRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _NetworkService_Delete_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(NetworkDeleteRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(NetworkServiceServer).Delete(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/pb.NetworkService/Delete",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(NetworkServiceServer).Delete(ctx, req.(*NetworkDeleteRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+var _NetworkService_serviceDesc = grpc.ServiceDesc{
+	ServiceName: "pb.NetworkService",
+	HandlerType: (*NetworkServiceServer)(nil),
+	Methods: []grpc.MethodDesc{
+		{
+			MethodName: "Create",
+			Handler:    _NetworkService_Create_Handler,
+		},
+		{
+			MethodName: "List",
+			Handler:    _NetworkService_List_Handler,
+		},
+		{
+			MethodName: "Delete",
+			Handler:    _NetworkService_Delete_Handler,
+		},
+	},
+	Streams:  []grpc.StreamDesc{},
+	Metadata: "network.proto",
+}
+
+func init() { proto.RegisterFile("network.proto", fileDescriptor2) }
+
+var fileDescriptor2 = []byte{
+	// 273 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0xcd, 0x4b, 0x2d, 0x29,
+	0xcf, 0x2f, 0xca, 0xd6, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x2a, 0x48, 0x52, 0xb2, 0xe3,
+	0x12, 0xf1, 0x83, 0x08, 0x3a, 0x17, 0xa5, 0x26, 0x96, 0xa4, 0x06, 0xa5, 0x16, 0x96, 0xa6, 0x16,
+	0x97, 0x08, 0x09, 0x71, 0xb1, 0xf8, 0x25, 0xe6, 0xa6, 0x4a, 0x30, 0x2a, 0x30, 0x6a, 0x70, 0x06,
+	0x81, 0xd9, 0x20, 0x31, 0x67, 0x4f, 0x97, 0x20, 0x09, 0x26, 0x88, 0x18, 0x88, 0xad, 0x24, 0xc2,
+	0x25, 0x04, 0xd5, 0xef, 0x93, 0x59, 0x5c, 0x02, 0xd5, 0xad, 0xa4, 0x05, 0x37, 0xd5, 0x25, 0x35,
+	0x27, 0x15, 0xaf, 0xa9, 0x4a, 0xfe, 0x5c, 0xec, 0x50, 0xb5, 0xc4, 0x5a, 0x2a, 0x24, 0xc3, 0xc5,
+	0x09, 0x71, 0x6d, 0x8a, 0x63, 0x89, 0x04, 0x33, 0x58, 0x02, 0x21, 0xa0, 0x64, 0xc7, 0x25, 0x8a,
+	0xe6, 0xa5, 0xe2, 0x82, 0xfc, 0xbc, 0xe2, 0x54, 0x21, 0x55, 0xb8, 0x4d, 0x60, 0x1b, 0xb8, 0x8d,
+	0xb8, 0xf5, 0x0a, 0x92, 0xf4, 0xa0, 0x42, 0x41, 0x30, 0x39, 0x25, 0x3b, 0x2e, 0x61, 0x14, 0x2f,
+	0x41, 0x75, 0xab, 0x73, 0x71, 0x40, 0x85, 0x8b, 0x25, 0x18, 0x15, 0x98, 0xd1, 0xb5, 0xc3, 0x25,
+	0x91, 0xec, 0x87, 0x79, 0x9e, 0x24, 0xfb, 0x8d, 0xce, 0x32, 0x72, 0xf1, 0x41, 0xd9, 0xc1, 0xa9,
+	0x45, 0x65, 0x99, 0xc9, 0xa9, 0x42, 0xf6, 0x5c, 0x6c, 0x10, 0xbf, 0x08, 0x49, 0x20, 0x69, 0x41,
+	0x89, 0x31, 0x29, 0x49, 0x2c, 0x32, 0x10, 0x8b, 0x95, 0x18, 0x84, 0x2c, 0xb9, 0x58, 0x40, 0x9e,
+	0x11, 0x12, 0x43, 0x52, 0x84, 0x14, 0x61, 0x52, 0xe2, 0x18, 0xe2, 0x70, 0xad, 0xf6, 0x5c, 0x6c,
+	0x10, 0x7f, 0xa0, 0xd8, 0x8d, 0x12, 0xaf, 0x28, 0x76, 0xa3, 0x7a, 0x5a, 0x89, 0x21, 0x89, 0x0d,
+	0x9c, 0xda, 0x8c, 0x01, 0x01, 0x00, 0x00, 0xff, 0xff, 0xff, 0xf0, 0x5e, 0xe8, 0x7e, 0x02, 0x00,
+	0x00,
+}

+ 33 - 0
pb/network.proto

@@ -0,0 +1,33 @@
+syntax = "proto3";
+
+package pb;
+
+message NetworkCreateRequest {
+  string Name = 1;
+  string CIDR = 2;
+}
+message NetworkListRequest {}
+message NetworkDeleteRequest {
+  string Name = 1;
+}
+
+service NetworkService {
+  rpc Create (NetworkCreateRequest) returns (NetworkCreateResponse) {}
+  rpc List (NetworkListRequest) returns (NetworkListResponse) {}
+  rpc Delete (NetworkDeleteRequest) returns (NetworkDeleteResponse) {}
+}
+message Network {
+  string Name = 1;
+  string CIDR = 2;
+  string CreatedAt = 3;
+}
+message NetworkCreateResponse {
+  Network Network = 1;
+}
+
+message NetworkListResponse {
+  repeated Network Networks = 1;
+}
+message NetworkDeleteResponse {
+  Network Network = 1;
+}

+ 8 - 0
pb/user.pb.go

@@ -7,6 +7,7 @@ Package pb is a generated protocol buffer package.
 It is generated from these files:
 	user.proto
 	vpn.proto
+	network.proto
 
 It has these top-level messages:
 	UserListRequest
@@ -21,6 +22,13 @@ It has these top-level messages:
 	VPNInitRequest
 	VPNStatusResponse
 	VPNInitResponse
+	NetworkCreateRequest
+	NetworkListRequest
+	NetworkDeleteRequest
+	Network
+	NetworkCreateResponse
+	NetworkListResponse
+	NetworkDeleteResponse
 */
 package pb
 

+ 1 - 11
vpn.go

@@ -1,5 +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
+//go:generate protoc -I pb/ pb/user.proto pb/vpn.proto pb/network.proto --go_out=plugins=grpc:pb
 
 package ovpm
 
@@ -23,16 +23,6 @@ import (
 	"github.com/jinzhu/gorm"
 )
 
-// DBNetwork is database model for external networks on the VPN server.
-type DBNetwork struct {
-	gorm.Model
-	ServerID uint
-	Server   DBServer
-
-	Name        string
-	NetworkCIDR string
-}
-
 // DBServer is database model for storing VPN server related stuff.
 type DBServer struct {
 	gorm.Model