Переглянути джерело

Merge branch 'release-v0.1.11'

Mustafa Arici 8 роки тому
батько
коміт
ddef0c43b2
21 змінених файлів з 415 додано та 179 видалено
  1. 20 2
      CHANGELOG.md
  2. 12 0
      README.md
  3. 6 2
      api/rpc.go
  4. 0 0
      bindata/bindata.go
  5. 3 4
      cmd/ovpm/net.go
  6. 60 17
      cmd/ovpm/user.go
  7. 17 8
      cmd/ovpm/vpn.go
  8. 7 4
      const.go
  9. 14 7
      net.go
  10. 7 7
      net_test.go
  11. 80 41
      pb/user.pb.go
  12. 6 0
      pb/user.proto
  13. 12 0
      pb/user.swagger.json
  14. 36 27
      pb/vpn.pb.go
  15. 1 0
      pb/vpn.proto
  16. 3 0
      pb/vpn.swagger.json
  17. 2 2
      template/server.conf.tmpl
  18. 31 19
      user.go
  19. 27 18
      user_test.go
  20. 59 9
      vpn.go
  21. 12 12
      vpn_test.go

+ 20 - 2
CHANGELOG.md

@@ -1,10 +1,29 @@
 # Change Log
 
-## [v0.1.10](https://github.com/cad/ovpm/tree/v0.1.10) (2017-08-27)
+## [v0.1.11](https://github.com/cad/ovpm/tree/v0.1.11) (2017-08-31)
+[Full Changelog](https://github.com/cad/ovpm/compare/v0.1.10...v0.1.11)
+
+**Implemented enhancements:**
+
+- be able to change initial ip block [\#29](https://github.com/cad/ovpm/issues/29)
+
+**Fixed bugs:**
+
+- can add duplicate static ip [\#37](https://github.com/cad/ovpm/issues/37)
+- net def --via flag doesn't work as documented [\#36](https://github.com/cad/ovpm/issues/36)
+- Error when group 'nobody' doesn't exist [\#32](https://github.com/cad/ovpm/issues/32)
+- --static option doesn't work when user update [\#28](https://github.com/cad/ovpm/issues/28)
+
+**Merged pull requests:**
+
+- openvpn user created by openvpn package, so use openvpn user instead. [\#35](https://github.com/cad/ovpm/pull/35) ([ilkerdagli](https://github.com/ilkerdagli))
+
+## [v0.1.10](https://github.com/cad/ovpm/tree/v0.1.10) (2017-08-29)
 [Full Changelog](https://github.com/cad/ovpm/compare/v0.1.9...v0.1.10)
 
 **Implemented enhancements:**
 
+- command line flags for tcp or udp at initialize [\#30](https://github.com/cad/ovpm/issues/30)
 - show network types in cli [\#27](https://github.com/cad/ovpm/issues/27)
 
 ## [v0.1.9](https://github.com/cad/ovpm/tree/v0.1.9) (2017-08-27)
@@ -76,4 +95,3 @@
 - implement remote control proto [\#8](https://github.com/cad/ovpm/issues/8)
 - write docs [\#4](https://github.com/cad/ovpm/issues/4)
 - write unit tests [\#3](https://github.com/cad/ovpm/issues/3)
-

+ 12 - 0
README.md

@@ -22,6 +22,18 @@ $ sudo yum-config-manager --add-repo https://cad.github.io/ovpm/rpm/ovpm.repo
 $ sudo yum install ovpm
 ```
 
+**from DEB (Ubuntu/DEBIAN):**
+
+This is tested only on Ubuntu >=16.04.3 LTS
+
+```bash
+# Add APT Repo
+$ sudo sh -c 'echo "deb [trusted=yes] https://cad.github.io/ovpm/deb/ ovpm main" >> /etc/apt/sources.list'
+
+# Install OVPM
+$ sudo yum install ovpm
+```
+
 **from Source (go get):**
 
 Only dependency for ovpm is **OpenVPN>=2.3**.

+ 6 - 2
api/rpc.go

@@ -76,7 +76,10 @@ func (s *UserService) Update(ctx context.Context, req *pb.UserUpdateRequest) (*p
 
 	}
 
-	user.Update(req.Password, noGW, req.HostID)
+	err = user.Update(req.Password, noGW, req.HostID)
+	if err != nil {
+		return nil, err
+	}
 	pbUser := pb.UserResponse_User{
 		Username:           user.GetUsername(),
 		ServerSerialNumber: user.GetServerSerialNumber(),
@@ -184,7 +187,8 @@ func (s *VPNService) Init(ctx context.Context, req *pb.VPNInitRequest) (*pb.VPNI
 	case pb.VPNProto_NOPREF:
 		proto = ovpm.UDPProto
 	}
-	if err := ovpm.Init(req.Hostname, req.Port, proto); err != nil {
+
+	if err := ovpm.Init(req.Hostname, req.Port, proto, req.IPBlock); err != nil {
 		logrus.Errorf("server can not be created: %v", err)
 	}
 	return &pb.VPNInitResponse{}, nil

Різницю між файлами не показано, бо вона завелика
+ 0 - 0
bindata/bindata.go


+ 3 - 4
cmd/ovpm/net.go

@@ -49,14 +49,13 @@ var netDefineCommand = cli.Command{
 
 		switch ovpm.NetworkTypeFromString(typ) {
 		case ovpm.ROUTE:
-			if via != "" && !govalidator.IsCIDR(via) {
-				fmt.Printf("validation error: `%s` must be a network in the CIDR form", via)
+			if via != "" && !govalidator.IsIPv4(via) {
+				fmt.Printf("validation error: `%s` must be a network in the IPv4 form", via)
 				fmt.Println()
 				fmt.Println(cli.ShowSubcommandHelp(c))
 				os.Exit(1)
-			} else {
-				via = ""
 			}
+
 		case ovpm.SERVERNET:
 			if via != "" {
 				fmt.Println("--via flag can only be used with --type ROUTE")

+ 60 - 17
cmd/ovpm/user.go

@@ -7,6 +7,7 @@ import (
 	"os"
 
 	"github.com/Sirupsen/logrus"
+	"github.com/asaskevich/govalidator"
 	"github.com/cad/ovpm"
 	"github.com/cad/ovpm/pb"
 	"github.com/olekukonko/tablewriter"
@@ -87,12 +88,17 @@ var userCreateCommand = cli.Command{
 			fmt.Println(cli.ShowSubcommandHelp(c))
 			os.Exit(1)
 		}
-
+		if static != "" && !govalidator.IsIPv4(static) {
+			fmt.Println("--static flag takes a valid ipv4 address")
+			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.Printf("can not parse %s as IPv4", static)
 				fmt.Println()
 				fmt.Println(cli.ShowSubcommandHelp(c))
 				os.Exit(1)
@@ -142,6 +148,10 @@ var userUpdateCommand = cli.Command{
 			Name:  "static",
 			Usage: "ip address for the vpn user",
 		},
+		cli.BoolFlag{
+			Name:  "no-static",
+			Usage: "do not set static ip address for the vpn user",
+		},
 	},
 	Action: func(c *cli.Context) error {
 		action = "user:update"
@@ -150,32 +160,64 @@ var userUpdateCommand = cli.Command{
 		nogw := c.Bool("no-gw")
 		gw := c.Bool("gw")
 		static := c.String("static")
+		noStatic := c.Bool("no-static")
 
 		if username == "" {
 			fmt.Println(cli.ShowSubcommandHelp(c))
 			os.Exit(1)
 		}
 
-		if !(password != "" || gw || nogw) {
+		// Check wether if all flags are are empty.
+		if !(password != "" || gw || nogw || static != "" || noStatic) {
 			fmt.Println("nothing is updated!")
 			fmt.Println()
 			fmt.Println(cli.ShowSubcommandHelp(c))
 			os.Exit(1)
 		}
 
+		// Given that static is set, check wether it's IPv4.
+		if static != "" && !govalidator.IsIPv4(static) {
+			fmt.Println("--static flag takes a valid ipv4 address")
+			fmt.Println()
+			fmt.Println(cli.ShowSubcommandHelp(c))
+			os.Exit(1)
+		}
+		var staticPref pb.UserUpdateRequest_StaticPref
+		staticPref = pb.UserUpdateRequest_NOPREFSTATIC
 		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
+		switch {
+		case static != "" && !noStatic:
+			// means static is set.
+			if static != "" {
+				h := ovpm.IP2HostID(net.ParseIP(static).To4())
+				if h == 0 {
+					fmt.Printf("can't parse %s as IPv4", static)
+					fmt.Println()
+					fmt.Println(cli.ShowSubcommandHelp(c))
+					os.Exit(1)
+				}
+
+				hostid = h
+			}
+			staticPref = pb.UserUpdateRequest_STATIC
+
+		case static == "" && noStatic:
+			// means no-static
+			hostid = 0
+			staticPref = pb.UserUpdateRequest_NOSTATIC
+		case static != "" && noStatic:
+			// means invalid
+			fmt.Println("--static flag and --no-static flag cannot be used together")
+			fmt.Println()
+			fmt.Println(cli.ShowSubcommandHelp(c))
+			os.Exit(1)
+		case static == "" && !noStatic:
+		default:
+			// means no pref
+			staticPref = pb.UserUpdateRequest_NOPREFSTATIC
+			hostid = 0
 		}
-
 		var gwPref pb.UserUpdateRequest_GWPref
 
 		switch {
@@ -200,10 +242,11 @@ var userUpdateCommand = cli.Command{
 		userSvc := pb.NewUserServiceClient(conn)
 
 		response, err := userSvc.Update(context.Background(), &pb.UserUpdateRequest{
-			Username: username,
-			Password: password,
-			Gwpref:   gwPref,
-			HostID:   hostid,
+			Username:   username,
+			Password:   password,
+			Gwpref:     gwPref,
+			HostID:     hostid,
+			Staticpref: staticPref,
 		})
 
 		if err != nil {

+ 17 - 8
cmd/ovpm/vpn.go

@@ -6,6 +6,7 @@ import (
 	"os"
 
 	"github.com/Sirupsen/logrus"
+	"github.com/asaskevich/govalidator"
 	"github.com/cad/ovpm"
 	"github.com/cad/ovpm/pb"
 	"github.com/olekukonko/tablewriter"
@@ -60,12 +61,16 @@ var vpnInitCommand = cli.Command{
 			Name:  "tcp, t",
 			Usage: "use TCP for vpn protocol, instead of UDP",
 		},
+		cli.StringFlag{
+			Name:  "net, n",
+			Usage: fmt.Sprintf("VPN network to give clients IP addresses from, in the CIDR form (default: %s)", ovpm.DefaultVPNNetwork),
+		},
 	},
 	Action: func(c *cli.Context) error {
 		action = "vpn:init"
 		hostname := c.String("hostname")
 		if hostname == "" {
-			logrus.Errorf("'hostname' is needed")
+			logrus.Errorf("'hostname' is required")
 			fmt.Println(cli.ShowSubcommandHelp(c))
 			os.Exit(1)
 
@@ -78,13 +83,17 @@ var vpnInitCommand = cli.Command{
 
 		tcp := c.Bool("tcp")
 
-		var proto pb.VPNProto
-
-		switch tcp {
-		case true:
+		proto := pb.VPNProto_UDP
+		if tcp {
 			proto = pb.VPNProto_TCP
-		default:
-			proto = pb.VPNProto_UDP
+		}
+
+		ipblock := c.String("net")
+		if ipblock != "" && !govalidator.IsCIDR(ipblock) {
+			fmt.Println("--net takes an ip network in the CIDR form. e.g. 10.9.0.0/24")
+			fmt.Println()
+			fmt.Println(cli.ShowSubcommandHelp(c))
+			os.Exit(1)
 		}
 
 		conn := getConn(c.GlobalString("daemon-port"))
@@ -106,7 +115,7 @@ var vpnInitCommand = cli.Command{
 			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, Protopref: proto}); err != nil {
+				if _, err := vpnSvc.Init(context.Background(), &pb.VPNInitRequest{Hostname: hostname, Port: port, Protopref: proto, IPBlock: ipblock}); err != nil {
 					logrus.Errorf("server can not be initialized: %v", err)
 					os.Exit(1)
 					return err

+ 7 - 4
const.go

@@ -2,11 +2,17 @@ package ovpm
 
 const (
 	// Version defines the version of ovpm.
-	Version = "0.1.10"
+	Version = "0.1.11"
 
 	// DefaultVPNPort is the default OpenVPN port to listen.
 	DefaultVPNPort = "1197"
 
+	// DefaultVPNProto is the default OpenVPN protocol to use.
+	DefaultVPNProto = UDPProto
+
+	// DefaultVPNNetwork is the default OpenVPN network to use.
+	DefaultVPNNetwork = "10.9.0.0/24"
+
 	etcBasePath = "/etc/ovpm/"
 	varBasePath = "/var/db/ovpm/"
 
@@ -20,9 +26,6 @@ const (
 	_DefaultCAKeyPath    = varBasePath + "ca.key"
 	_DefaultDHParamsPath = varBasePath + "dh4096.pem"
 	_DefaultCRLPath      = varBasePath + "crl.pem"
-
-	_DefaultServerNetwork = "10.9.0.0"
-	_DefaultServerNetMask = "255.255.255.0"
 )
 
 // Testing is used to determine wether we are testing or running normally.

+ 14 - 7
net.go

@@ -133,8 +133,8 @@ func CreateNewNetwork(name, cidr string, nettype NetworkType, via string) (*DBNe
 		return nil, fmt.Errorf("validation error: `%s` must be a network in the CIDR form", cidr)
 	}
 
-	if !govalidator.IsCIDR(via) && via != "" {
-		return nil, fmt.Errorf("validation error: `%s` must be a network in the CIDR form", via)
+	if via != "" && !govalidator.IsIPv4(via) {
+		return nil, fmt.Errorf("validation error: `%s` must be a network in the IPv4 form", via)
 	}
 
 	if nettype == UNDEFINEDNET {
@@ -146,13 +146,13 @@ func CreateNewNetwork(name, cidr string, nettype NetworkType, via string) (*DBNe
 		return nil, fmt.Errorf("can not parse CIDR %s: %v", cidr, err)
 	}
 
-	// Overwrite via with the parsed CIDR string.
+	// Overwrite via with the parsed IPv4 string.
 	if nettype == ROUTE && via != "" {
-		_, viaNet, err := net.ParseCIDR(via)
+		viaIP := net.ParseIP(via).To4()
 		if err != nil {
-			return nil, fmt.Errorf("can not parse CIDR %s: %v", via, err)
+			return nil, fmt.Errorf("can not parse IPv4 %s: %v", via, err)
 		}
-		via = viaNet.String()
+		via = viaIP.String()
 
 	} else {
 		via = ""
@@ -507,7 +507,7 @@ func HostID2IP(hostid uint32) net.IP {
 	return net.IP(ip)
 }
 
-//IP2HostID converts an IP address to a host id (32-bit unsigned integer).
+// 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
@@ -515,6 +515,13 @@ func IP2HostID(ip net.IP) uint32 {
 
 // IncrementIP will return next ip address within the network.
 func IncrementIP(ip, mask string) (string, error) {
+	if !govalidator.IsIPv4(ip) {
+		return "", fmt.Errorf("'ip' is expected to be a valid IPv4 %s", ip)
+	}
+	if !govalidator.IsIPv4(ip) {
+		return "", fmt.Errorf("'mask' is expected to be a valid IPv4 %s", mask)
+	}
+
 	ipAddr := net.ParseIP(ip).To4()
 	netMask := net.IPMask(net.ParseIP(mask).To4())
 	ipNet := net.IPNet{IP: ipAddr, Mask: netMask}

+ 7 - 7
net_test.go

@@ -9,7 +9,7 @@ func TestVPNCreateNewNetwork(t *testing.T) {
 	setupTestCase()
 	SetupDB("sqlite3", ":memory:")
 	defer CeaseDB()
-	Init("localhost", "", UDPProto)
+	Init("localhost", "", UDPProto, "")
 
 	// Prepare:
 	// Test:
@@ -56,7 +56,7 @@ func TestVPNDeleteNetwork(t *testing.T) {
 	setupTestCase()
 	SetupDB("sqlite3", ":memory:")
 	defer CeaseDB()
-	Init("localhost", "", UDPProto)
+	Init("localhost", "", UDPProto, "")
 
 	// Prepare:
 	// Test:
@@ -94,7 +94,7 @@ func TestVPNGetNetwork(t *testing.T) {
 	setupTestCase()
 	SetupDB("sqlite3", ":memory:")
 	defer CeaseDB()
-	Init("localhost", "", UDPProto)
+	Init("localhost", "", UDPProto, "")
 
 	// Prepare:
 	// Test:
@@ -129,7 +129,7 @@ func TestVPNGetAllNetworks(t *testing.T) {
 	setupTestCase()
 	SetupDB("sqlite3", ":memory:")
 	defer CeaseDB()
-	Init("localhost", "", UDPProto)
+	Init("localhost", "", UDPProto, "")
 
 	// Prepare:
 	// Test:
@@ -175,7 +175,7 @@ func TestNetAssociate(t *testing.T) {
 	setupTestCase()
 	SetupDB("sqlite3", ":memory:")
 	defer CeaseDB()
-	Init("localhost", "", UDPProto)
+	Init("localhost", "", UDPProto, "")
 
 	// Prepare:
 	// Test:
@@ -213,7 +213,7 @@ func TestNetDissociate(t *testing.T) {
 	setupTestCase()
 	SetupDB("sqlite3", ":memory:")
 	defer CeaseDB()
-	err := Init("localhost", "", UDPProto)
+	err := Init("localhost", "", UDPProto, "")
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -266,7 +266,7 @@ func TestNetGetAssociatedUsers(t *testing.T) {
 	setupTestCase()
 	SetupDB("sqlite3", ":memory:")
 	defer CeaseDB()
-	Init("localhost", "", UDPProto)
+	Init("localhost", "", UDPProto, "")
 
 	// Prepare:
 	// Test:

+ 80 - 41
pb/user.pb.go

@@ -86,6 +86,32 @@ func (x UserUpdateRequest_GWPref) String() string {
 }
 func (UserUpdateRequest_GWPref) EnumDescriptor() ([]byte, []int) { return fileDescriptor0, []int{2, 0} }
 
+type UserUpdateRequest_StaticPref int32
+
+const (
+	UserUpdateRequest_NOPREFSTATIC UserUpdateRequest_StaticPref = 0
+	UserUpdateRequest_NOSTATIC     UserUpdateRequest_StaticPref = 1
+	UserUpdateRequest_STATIC       UserUpdateRequest_StaticPref = 2
+)
+
+var UserUpdateRequest_StaticPref_name = map[int32]string{
+	0: "NOPREFSTATIC",
+	1: "NOSTATIC",
+	2: "STATIC",
+}
+var UserUpdateRequest_StaticPref_value = map[string]int32{
+	"NOPREFSTATIC": 0,
+	"NOSTATIC":     1,
+	"STATIC":       2,
+}
+
+func (x UserUpdateRequest_StaticPref) String() string {
+	return proto.EnumName(UserUpdateRequest_StaticPref_name, int32(x))
+}
+func (UserUpdateRequest_StaticPref) EnumDescriptor() ([]byte, []int) {
+	return fileDescriptor0, []int{2, 1}
+}
+
 type UserListRequest struct {
 }
 
@@ -135,10 +161,11 @@ func (m *UserCreateRequest) GetHostID() uint32 {
 }
 
 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"`
+	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"`
+	Staticpref UserUpdateRequest_StaticPref `protobuf:"varint,5,opt,name=staticpref,enum=pb.UserUpdateRequest_StaticPref" json:"staticpref,omitempty"`
 }
 
 func (m *UserUpdateRequest) Reset()                    { *m = UserUpdateRequest{} }
@@ -174,6 +201,13 @@ func (m *UserUpdateRequest) GetHostID() uint32 {
 	return 0
 }
 
+func (m *UserUpdateRequest) GetStaticpref() UserUpdateRequest_StaticPref {
+	if m != nil {
+		return m.Staticpref
+	}
+	return UserUpdateRequest_NOPREFSTATIC
+}
+
 type UserDeleteRequest struct {
 	Username string `protobuf:"bytes,1,opt,name=Username" json:"Username,omitempty"`
 }
@@ -329,6 +363,7 @@ func init() {
 	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)
+	proto.RegisterEnum("pb.UserUpdateRequest_StaticPref", UserUpdateRequest_StaticPref_name, UserUpdateRequest_StaticPref_value)
 }
 
 // Reference imports to suppress errors if they are not otherwise used.
@@ -571,41 +606,45 @@ var _UserService_serviceDesc = grpc.ServiceDesc{
 func init() { proto.RegisterFile("user.proto", fileDescriptor0) }
 
 var fileDescriptor0 = []byte{
-	// 575 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x94, 0x4d, 0x6e, 0xd3, 0x40,
-	0x14, 0xc7, 0xb1, 0x9b, 0x98, 0xe4, 0xf5, 0x2b, 0x9d, 0xa6, 0xe0, 0x5a, 0x41, 0x8a, 0x66, 0x81,
-	0xa2, 0x22, 0xd9, 0x22, 0xb0, 0x2a, 0x2b, 0x94, 0x82, 0x29, 0x82, 0x34, 0x72, 0x55, 0x65, 0x89,
-	0x9c, 0xe4, 0x25, 0xb2, 0x94, 0x7a, 0xcc, 0xcc, 0xa4, 0xd9, 0x23, 0x6e, 0xc0, 0x89, 0xd8, 0x70,
-	0x01, 0xae, 0xc0, 0x01, 0x38, 0x02, 0x9a, 0x19, 0xc7, 0x21, 0x21, 0xa0, 0x2c, 0xd8, 0xcd, 0xfb,
-	0xfa, 0x8d, 0xdf, 0x7b, 0xff, 0x31, 0xc0, 0x4c, 0x20, 0xf7, 0x33, 0xce, 0x24, 0x23, 0x76, 0x36,
-	0xf0, 0x1a, 0x13, 0xc6, 0x26, 0x53, 0x0c, 0xe2, 0x2c, 0x09, 0xe2, 0x34, 0x65, 0x32, 0x96, 0x09,
-	0x4b, 0x85, 0xc9, 0xa0, 0x47, 0x70, 0x78, 0x23, 0x90, 0xbf, 0x4b, 0x84, 0x8c, 0xf0, 0xe3, 0x0c,
-	0x85, 0xa4, 0x73, 0x38, 0x52, 0xae, 0x0e, 0xc7, 0x58, 0x62, 0xee, 0x24, 0x1e, 0x54, 0x94, 0x33,
-	0x8d, 0x6f, 0xd1, 0xb5, 0x9a, 0x56, 0xab, 0x1a, 0x15, 0xb6, 0x8a, 0xf5, 0x62, 0x21, 0xe6, 0x8c,
-	0x8f, 0x5c, 0xdb, 0xc4, 0x16, 0x36, 0x21, 0x50, 0xea, 0xb2, 0xb0, 0xef, 0xee, 0x34, 0xad, 0x56,
-	0x25, 0xd2, 0x67, 0xf2, 0x00, 0x9c, 0x37, 0x4c, 0xc8, 0xcb, 0x0b, 0xb7, 0xd4, 0xb4, 0x5a, 0xfb,
-	0x51, 0x6e, 0xd1, 0xaf, 0x96, 0xb9, 0xf9, 0x26, 0x1b, 0xfd, 0x87, 0x9b, 0x9f, 0x83, 0x33, 0x99,
-	0x67, 0x1c, 0xc7, 0xfa, 0xee, 0x83, 0x76, 0xc3, 0xcf, 0x06, 0xfe, 0x1f, 0x78, 0x3f, 0xec, 0xf7,
-	0x38, 0x8e, 0xa3, 0x3c, 0xf7, 0xaf, 0xdf, 0xf6, 0x18, 0x1c, 0x93, 0x49, 0x00, 0x9c, 0xee, 0x55,
-	0x2f, 0x7a, 0xf5, 0xba, 0x76, 0x8f, 0x54, 0xa0, 0xd4, 0xbd, 0x0a, 0xfb, 0x35, 0x8b, 0x38, 0x60,
-	0x87, 0xfd, 0x9a, 0x4d, 0x03, 0xd3, 0xc2, 0x05, 0x4e, 0x71, 0xab, 0x16, 0xa8, 0x0f, 0x35, 0x75,
-	0x8e, 0x30, 0xc5, 0xf9, 0x36, 0xf9, 0x6d, 0xa8, 0xab, 0x73, 0x88, 0x69, 0x87, 0xa5, 0xe3, 0x64,
-	0xb2, 0x4d, 0xcd, 0x67, 0x1b, 0xf6, 0xcc, 0x25, 0x22, 0x63, 0xa9, 0x40, 0xf2, 0x04, 0xca, 0x4a,
-	0x25, 0xc2, 0xb5, 0x9a, 0x3b, 0xad, 0xdd, 0xf6, 0xc9, 0x62, 0x34, 0x8b, 0x04, 0x63, 0x98, 0x1c,
-	0xef, 0x9b, 0x05, 0x25, 0x65, 0xff, 0x73, 0x13, 0x3e, 0x90, 0x6b, 0xe4, 0x77, 0xc8, 0xaf, 0x91,
-	0x27, 0xf1, 0xb4, 0x3b, 0xbb, 0x1d, 0x20, 0xcf, 0x77, 0xb2, 0x21, 0xa2, 0x74, 0xd1, 0x41, 0x2e,
-	0xf5, 0x6e, 0xaa, 0x91, 0x3e, 0x93, 0x06, 0x54, 0x8d, 0xe8, 0x46, 0x2f, 0xa5, 0x1e, 0x7f, 0x35,
-	0x5a, 0x3a, 0x48, 0x1d, 0xca, 0x97, 0xbd, 0x2e, 0x4a, 0xb7, 0xac, 0x23, 0xc6, 0x28, 0xf4, 0xe5,
-	0x6c, 0xd4, 0xd7, 0xfd, 0x95, 0x1d, 0xbe, 0x80, 0x93, 0xb5, 0xd1, 0xe5, 0xe3, 0xa0, 0xb0, 0xd7,
-	0x99, 0x26, 0x98, 0x4a, 0xe3, 0xcf, 0x9b, 0x5b, 0xf1, 0xb5, 0x7f, 0xee, 0xc0, 0xae, 0xaa, 0x56,
-	0xbd, 0x24, 0x43, 0x24, 0x21, 0x94, 0xd4, 0xa3, 0x21, 0xc7, 0x8b, 0xd9, 0xfd, 0xf6, 0x84, 0xbc,
-	0xda, 0xfa, 0x40, 0xa9, 0xfb, 0xe9, 0xfb, 0x8f, 0x2f, 0x36, 0xa1, 0xfb, 0xc1, 0xdd, 0xd3, 0x40,
-	0xcd, 0x35, 0x98, 0x26, 0x42, 0x9e, 0x5b, 0x67, 0xe4, 0x3d, 0x38, 0xa6, 0x49, 0x52, 0xac, 0x61,
-	0xe5, 0xe9, 0x6d, 0x80, 0x79, 0x1a, 0x56, 0xa7, 0x87, 0x05, 0x6c, 0xa8, 0x2b, 0x72, 0x9c, 0x11,
-	0xf8, 0x12, 0xb7, 0x22, 0xf8, 0xad, 0x70, 0x33, 0x5d, 0x91, 0xe3, 0x8c, 0x96, 0x97, 0xb8, 0x15,
-	0x6d, 0x6f, 0x85, 0x1b, 0xe9, 0x0a, 0x85, 0x7b, 0x0b, 0x65, 0xad, 0x74, 0x52, 0x5f, 0x96, 0x2d,
-	0x85, 0xbf, 0x01, 0x76, 0xaa, 0x61, 0xc7, 0xf4, 0xa0, 0x80, 0x71, 0x55, 0xa0, 0x58, 0x1f, 0xa0,
-	0x5a, 0xac, 0x92, 0xb8, 0x8b, 0xca, 0xf5, 0x87, 0xe1, 0x9d, 0x6e, 0x88, 0xe4, 0xf0, 0x47, 0x1a,
-	0xfe, 0x90, 0x92, 0x02, 0x3e, 0xc1, 0x74, 0xa8, 0x73, 0xce, 0xad, 0xb3, 0x81, 0xa3, 0x7f, 0x91,
-	0xcf, 0x7e, 0x05, 0x00, 0x00, 0xff, 0xff, 0xc8, 0x3d, 0xc7, 0xac, 0x52, 0x05, 0x00, 0x00,
+	// 628 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x94, 0xc1, 0x6e, 0xd3, 0x4c,
+	0x10, 0xc7, 0x6b, 0x37, 0xf1, 0x97, 0x4c, 0xd3, 0xd6, 0xdd, 0xa6, 0x1f, 0x6e, 0x54, 0xa4, 0x68,
+	0x0f, 0x28, 0x2a, 0x52, 0x22, 0x02, 0x07, 0x54, 0x2e, 0x54, 0x29, 0x98, 0x22, 0x70, 0x23, 0xa7,
+	0x55, 0x8e, 0xc8, 0x49, 0xa6, 0x91, 0xa5, 0xd4, 0x36, 0xbb, 0x9b, 0xe6, 0x8e, 0x78, 0x03, 0x9e,
+	0x87, 0x2b, 0x2f, 0xc0, 0x2b, 0xf0, 0x00, 0x3c, 0x02, 0xda, 0x5d, 0xc7, 0x6e, 0x8a, 0x41, 0x39,
+	0x70, 0x9b, 0x99, 0x9d, 0xf9, 0x8d, 0x77, 0xf6, 0x3f, 0x06, 0x98, 0x73, 0x64, 0xed, 0x84, 0xc5,
+	0x22, 0x26, 0x66, 0x32, 0x6a, 0x1c, 0x4d, 0xe3, 0x78, 0x3a, 0xc3, 0x4e, 0x90, 0x84, 0x9d, 0x20,
+	0x8a, 0x62, 0x11, 0x88, 0x30, 0x8e, 0xb8, 0xce, 0xa0, 0x7b, 0xb0, 0x7b, 0xc5, 0x91, 0xbd, 0x0b,
+	0xb9, 0xf0, 0xf1, 0xe3, 0x1c, 0xb9, 0xa0, 0x0b, 0xd8, 0x93, 0xa1, 0x1e, 0xc3, 0x40, 0x60, 0x1a,
+	0x24, 0x0d, 0xa8, 0xc8, 0x60, 0x14, 0xdc, 0xa0, 0x63, 0x34, 0x8d, 0x56, 0xd5, 0xcf, 0x7c, 0x79,
+	0xd6, 0x0f, 0x38, 0x5f, 0xc4, 0x6c, 0xe2, 0x98, 0xfa, 0x6c, 0xe9, 0x13, 0x02, 0x25, 0x2f, 0x76,
+	0x87, 0xce, 0x66, 0xd3, 0x68, 0x55, 0x7c, 0x65, 0x93, 0xff, 0xc1, 0x7a, 0x13, 0x73, 0x71, 0x7e,
+	0xe6, 0x94, 0x9a, 0x46, 0x6b, 0xdb, 0x4f, 0x3d, 0xfa, 0xd5, 0xd4, 0x9d, 0xaf, 0x92, 0xc9, 0x3f,
+	0xe8, 0xfc, 0x0c, 0xac, 0xe9, 0x22, 0x61, 0x78, 0xad, 0x7a, 0xef, 0x74, 0x8f, 0xda, 0xc9, 0xa8,
+	0xfd, 0x1b, 0xbe, 0xed, 0x0e, 0xfb, 0x0c, 0xaf, 0xfd, 0x34, 0xf7, 0x4f, 0xdf, 0x46, 0x5e, 0x02,
+	0x70, 0x39, 0xb9, 0xb1, 0x22, 0x96, 0x15, 0xb1, 0x59, 0x4c, 0x1c, 0xa8, 0x3c, 0x45, 0xbd, 0x53,
+	0x43, 0x1f, 0x81, 0xa5, 0x7b, 0x11, 0x00, 0xcb, 0xbb, 0xe8, 0xfb, 0xaf, 0x5e, 0xdb, 0x1b, 0xa4,
+	0x02, 0x25, 0xef, 0xc2, 0x1d, 0xda, 0x06, 0xb1, 0xc0, 0x74, 0x87, 0xb6, 0x49, 0x9f, 0x03, 0xe4,
+	0x04, 0x62, 0x43, 0x4d, 0xe7, 0x0e, 0x2e, 0x4f, 0x2f, 0xcf, 0x7b, 0xf6, 0x06, 0xa9, 0x41, 0xc5,
+	0xbb, 0x48, 0x3d, 0x43, 0xb2, 0x52, 0xdb, 0xa4, 0x1d, 0x3d, 0xbe, 0x33, 0x9c, 0xe1, 0x5a, 0xe3,
+	0xa3, 0x6d, 0xb0, 0xa5, 0xed, 0x63, 0x84, 0x8b, 0x75, 0xf2, 0xbb, 0x50, 0x97, 0xb6, 0x8b, 0x51,
+	0x2f, 0x8e, 0xae, 0xc3, 0xe9, 0x3a, 0x35, 0x9f, 0x4d, 0xa8, 0xe9, 0x26, 0x3c, 0x89, 0x23, 0x8e,
+	0xe4, 0x31, 0x94, 0xa5, 0x42, 0xb9, 0x63, 0x34, 0x37, 0x5b, 0x5b, 0xdd, 0x83, 0xe5, 0x10, 0x97,
+	0x09, 0xda, 0xd1, 0x39, 0x8d, 0x6f, 0x06, 0x94, 0xa4, 0xff, 0x57, 0x15, 0xb4, 0x81, 0x0c, 0x90,
+	0xdd, 0x22, 0x1b, 0x20, 0x0b, 0x83, 0x99, 0x37, 0xbf, 0x19, 0x21, 0x4b, 0xf5, 0x50, 0x70, 0x22,
+	0x35, 0xd9, 0x43, 0x26, 0x94, 0x2e, 0xaa, 0xbe, 0xb2, 0xc9, 0x11, 0x54, 0xb5, 0xe0, 0x27, 0xa7,
+	0x42, 0x3d, 0x7d, 0xd5, 0xcf, 0x03, 0xa4, 0x0e, 0xe5, 0xf3, 0xbe, 0x87, 0x42, 0x3d, 0x7c, 0xd5,
+	0xd7, 0x4e, 0xa6, 0x6d, 0xab, 0x50, 0xdb, 0xff, 0xad, 0x68, 0xfb, 0x05, 0x1c, 0xdc, 0x1b, 0x5d,
+	0x3a, 0x0e, 0x0a, 0xb5, 0xde, 0x2c, 0xc4, 0x48, 0xe8, 0x78, 0x7a, 0xb9, 0x95, 0x58, 0xf7, 0xe7,
+	0x26, 0x6c, 0xc9, 0x6a, 0x79, 0x97, 0x70, 0x8c, 0xc4, 0x85, 0x92, 0x5c, 0x58, 0xb2, 0xbf, 0x9c,
+	0xdd, 0x9d, 0xf5, 0x6d, 0xd8, 0xf7, 0x07, 0x4a, 0x9d, 0x4f, 0xdf, 0x7f, 0x7c, 0x31, 0x09, 0xdd,
+	0xee, 0xdc, 0x3e, 0xe9, 0xc8, 0xb9, 0x76, 0x66, 0x21, 0x17, 0x27, 0xc6, 0x31, 0x79, 0x0f, 0x96,
+	0xbe, 0x24, 0xc9, 0x9e, 0x61, 0x65, 0xed, 0x0b, 0x60, 0x0d, 0x05, 0xab, 0xd3, 0xdd, 0x0c, 0x36,
+	0x56, 0x15, 0x29, 0x4e, 0xaf, 0x42, 0x8e, 0x5b, 0x59, 0x8d, 0xb5, 0x70, 0x73, 0x55, 0x91, 0xe2,
+	0xb4, 0x96, 0x73, 0xdc, 0x8a, 0xb6, 0xd7, 0xc2, 0x4d, 0x54, 0x85, 0xc4, 0xbd, 0x85, 0xb2, 0x52,
+	0x3a, 0xa9, 0xe7, 0x65, 0xb9, 0xf0, 0x0b, 0x60, 0x87, 0x0a, 0xb6, 0x4f, 0x77, 0x32, 0x18, 0x93,
+	0x05, 0x92, 0xf5, 0x01, 0xaa, 0xd9, 0x53, 0x12, 0x67, 0x59, 0x79, 0x7f, 0x31, 0x1a, 0x87, 0x05,
+	0x27, 0x29, 0xfc, 0xa1, 0x82, 0x3f, 0xa0, 0x24, 0x83, 0x4f, 0x31, 0x1a, 0xab, 0x9c, 0x13, 0xe3,
+	0x78, 0x64, 0xa9, 0xdf, 0xf3, 0xd3, 0x5f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x92, 0x5d, 0x0b, 0xf4,
+	0xce, 0x05, 0x00, 0x00,
 }

+ 6 - 0
pb/user.proto

@@ -25,6 +25,12 @@ message UserUpdateRequest {
   }
   GWPref gwpref = 3;
   uint32 HostID = 4;
+  enum StaticPref {
+    NOPREFSTATIC = 0;
+    NOSTATIC = 1;
+    STATIC = 2;
+  }
+  StaticPref staticpref = 5;
 }
 
 

+ 12 - 0
pb/user.swagger.json

@@ -210,6 +210,15 @@
       ],
       "default": "NOPREF"
     },
+    "UserUpdateRequestStaticPref": {
+      "type": "string",
+      "enum": [
+        "NOPREFSTATIC",
+        "NOSTATIC",
+        "STATIC"
+      ],
+      "default": "NOPREFSTATIC"
+    },
     "pbUserCreateRequest": {
       "type": "object",
       "properties": {
@@ -290,6 +299,9 @@
         "HostID": {
           "type": "integer",
           "format": "int64"
+        },
+        "staticpref": {
+          "$ref": "#/definitions/UserUpdateRequestStaticPref"
         }
       }
     }

+ 36 - 27
pb/vpn.pb.go

@@ -54,6 +54,7 @@ type VPNInitRequest struct {
 	Hostname  string   `protobuf:"bytes,1,opt,name=Hostname" json:"Hostname,omitempty"`
 	Port      string   `protobuf:"bytes,2,opt,name=Port" json:"Port,omitempty"`
 	Protopref VPNProto `protobuf:"varint,3,opt,name=Protopref,enum=pb.VPNProto" json:"Protopref,omitempty"`
+	IPBlock   string   `protobuf:"bytes,4,opt,name=IPBlock" json:"IPBlock,omitempty"`
 }
 
 func (m *VPNInitRequest) Reset()                    { *m = VPNInitRequest{} }
@@ -82,6 +83,13 @@ func (m *VPNInitRequest) GetProtopref() VPNProto {
 	return VPNProto_NOPREF
 }
 
+func (m *VPNInitRequest) GetIPBlock() string {
+	if m != nil {
+		return m.IPBlock
+	}
+	return ""
+}
+
 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"`
@@ -294,31 +302,32 @@ var _VPNService_serviceDesc = grpc.ServiceDesc{
 func init() { proto.RegisterFile("vpn.proto", fileDescriptor1) }
 
 var fileDescriptor1 = []byte{
-	// 406 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x92, 0xc1, 0x72, 0xd3, 0x30,
-	0x10, 0x86, 0xb1, 0x93, 0xba, 0xf1, 0x4e, 0x26, 0x38, 0xdb, 0x02, 0x22, 0xd3, 0x43, 0x47, 0xa7,
-	0x4c, 0x0e, 0xf1, 0x50, 0x6e, 0xbd, 0x75, 0x0c, 0x4c, 0x39, 0x60, 0x34, 0x2e, 0xe4, 0x2e, 0x83,
-	0xe8, 0x78, 0x48, 0x25, 0x21, 0x29, 0xbe, 0xc3, 0x2b, 0xf0, 0x12, 0xbc, 0x0f, 0xaf, 0xc0, 0x83,
-	0x30, 0x92, 0xdd, 0x26, 0x66, 0x86, 0xdb, 0xbf, 0xdf, 0x66, 0x77, 0xff, 0xfc, 0x16, 0xa4, 0xad,
-	0x96, 0x6b, 0x6d, 0x94, 0x53, 0x18, 0xeb, 0x7a, 0x71, 0x76, 0xab, 0xd4, 0xed, 0x56, 0xe4, 0x5c,
-	0x37, 0x39, 0x97, 0x52, 0x39, 0xee, 0x1a, 0x25, 0x6d, 0xf7, 0x0b, 0x8a, 0x90, 0x6d, 0x58, 0x79,
-	0xe3, 0xb8, 0xdb, 0xd9, 0x4a, 0x7c, 0xdb, 0x09, 0xeb, 0xe8, 0x16, 0x66, 0x1b, 0x56, 0xbe, 0x95,
-	0x8d, 0xeb, 0x09, 0x2e, 0x60, 0x72, 0xad, 0xac, 0x93, 0xfc, 0x4e, 0x90, 0xe8, 0x3c, 0x5a, 0xa6,
-	0xd5, 0x43, 0x8d, 0x08, 0x63, 0xa6, 0x8c, 0x23, 0x71, 0xe0, 0x41, 0xe3, 0x0a, 0x52, 0xe6, 0xd7,
-	0x6b, 0x23, 0xbe, 0x90, 0xd1, 0x79, 0xb4, 0x9c, 0x5d, 0x4c, 0xd7, 0xba, 0x5e, 0x6f, 0x58, 0x19,
-	0x78, 0xb5, 0x6f, 0xd3, 0xef, 0x31, 0xcc, 0x0f, 0x2c, 0x58, 0xad, 0xa4, 0x0d, 0x5b, 0xcb, 0xfd,
-	0xb5, 0xa0, 0x91, 0xc2, 0xf4, 0x46, 0x98, 0x86, 0x6f, 0xcb, 0xdd, 0x5d, 0x2d, 0x4c, 0x7f, 0x71,
-	0xc0, 0x06, 0x4e, 0x47, 0xff, 0x71, 0x3a, 0x3e, 0x70, 0x8a, 0x30, 0x2e, 0x84, 0x71, 0xe4, 0xa8,
-	0x63, 0x5e, 0xe3, 0x53, 0x48, 0x8a, 0xab, 0x40, 0x93, 0x40, 0xfb, 0x0a, 0x33, 0x18, 0x95, 0xc2,
-	0x91, 0xe3, 0x00, 0xbd, 0xf4, 0xd3, 0xef, 0xb8, 0xfd, 0x4a, 0x26, 0xdd, 0xb4, 0xd7, 0x78, 0x06,
-	0x69, 0x61, 0x04, 0x77, 0xe2, 0xf3, 0x95, 0x23, 0x69, 0x68, 0xec, 0x01, 0x9e, 0xc2, 0x51, 0xf8,
-	0xeb, 0x04, 0x42, 0xa7, 0x2b, 0xe8, 0x1c, 0x1e, 0x3f, 0x24, 0xde, 0x05, 0xb0, 0x5a, 0xc2, 0xe4,
-	0x3e, 0x2d, 0x04, 0x48, 0xca, 0xf7, 0xac, 0x7a, 0xfd, 0x26, 0x7b, 0x84, 0xc7, 0x30, 0xfa, 0xf8,
-	0x8a, 0x65, 0x91, 0x17, 0x1f, 0x0a, 0x96, 0xc5, 0x17, 0xbf, 0x22, 0x00, 0x1f, 0xa0, 0x30, 0x6d,
-	0xf3, 0x49, 0x20, 0x83, 0xa4, 0xcb, 0x12, 0x4f, 0xfb, 0xc8, 0x07, 0x5f, 0x77, 0xf1, 0xe4, 0x1f,
-	0xda, 0xdd, 0xa3, 0xcf, 0x7f, 0xfc, 0xfe, 0xf3, 0x33, 0x3e, 0xa1, 0xb3, 0xbc, 0x7d, 0x91, 0xb7,
-	0x5a, 0xe6, 0x36, 0xf4, 0x2f, 0xa3, 0x15, 0x5e, 0xc3, 0xd8, 0x5b, 0x43, 0xec, 0x27, 0x0f, 0x5e,
-	0xc6, 0xe2, 0x64, 0xc0, 0xfa, 0x5d, 0xcf, 0xc2, 0xae, 0x39, 0x9d, 0xde, 0xef, 0x6a, 0x64, 0xe3,
-	0x2e, 0xa3, 0x55, 0x9d, 0x84, 0x47, 0xf7, 0xf2, 0x6f, 0x00, 0x00, 0x00, 0xff, 0xff, 0xfc, 0x94,
-	0xe6, 0xba, 0xa3, 0x02, 0x00, 0x00,
+	// 423 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x92, 0xcf, 0x72, 0xd3, 0x30,
+	0x10, 0xc6, 0xb1, 0xe3, 0x3a, 0xf1, 0x4e, 0x26, 0x38, 0xdb, 0x02, 0x22, 0xd3, 0x43, 0x47, 0xa7,
+	0x4c, 0x0e, 0xf1, 0x50, 0x6e, 0xbd, 0x95, 0x00, 0xd3, 0x1e, 0x30, 0x9a, 0x14, 0x72, 0x57, 0x8a,
+	0xe8, 0x78, 0x9a, 0x4a, 0x42, 0x52, 0x72, 0x87, 0x03, 0x2f, 0xc0, 0x4b, 0xf0, 0x3e, 0xbc, 0x02,
+	0x0f, 0xc2, 0x48, 0x76, 0xfe, 0x98, 0x99, 0xde, 0xbe, 0xfd, 0xad, 0x77, 0xbd, 0xfb, 0xad, 0x20,
+	0xdb, 0x68, 0x39, 0xd5, 0x46, 0x39, 0x85, 0xb1, 0x5e, 0x8e, 0x4e, 0xef, 0x94, 0xba, 0x5b, 0x89,
+	0x82, 0xeb, 0xaa, 0xe0, 0x52, 0x2a, 0xc7, 0x5d, 0xa5, 0xa4, 0xad, 0xbf, 0xa0, 0x08, 0xf9, 0x82,
+	0x95, 0x37, 0x8e, 0xbb, 0xb5, 0x9d, 0x8b, 0x6f, 0x6b, 0x61, 0x1d, 0xfd, 0x19, 0xc1, 0x60, 0xc1,
+	0xca, 0x6b, 0x59, 0xb9, 0x06, 0xe1, 0x08, 0x7a, 0x57, 0xca, 0x3a, 0xc9, 0x1f, 0x04, 0x89, 0xce,
+	0xa2, 0x71, 0x36, 0xdf, 0xc5, 0x88, 0x90, 0x30, 0x65, 0x1c, 0x89, 0x03, 0x0f, 0x1a, 0x27, 0x90,
+	0x31, 0xdf, 0x5f, 0x1b, 0xf1, 0x95, 0x74, 0xce, 0xa2, 0xf1, 0xe0, 0xbc, 0x3f, 0xd5, 0xcb, 0xe9,
+	0x82, 0x95, 0x81, 0xcf, 0xf7, 0x69, 0x24, 0xd0, 0xbd, 0x66, 0x6f, 0x56, 0xea, 0xf6, 0x9e, 0x24,
+	0xa1, 0xc5, 0x36, 0xa4, 0xdf, 0x63, 0x18, 0x1e, 0x4c, 0x67, 0xb5, 0x92, 0x36, 0xfc, 0xaf, 0xdc,
+	0xcf, 0x11, 0x34, 0x52, 0xe8, 0xdf, 0x08, 0x53, 0xf1, 0x55, 0xb9, 0x7e, 0x58, 0x0a, 0xd3, 0xcc,
+	0xd2, 0x62, 0xad, 0x1d, 0x3a, 0x8f, 0xec, 0x90, 0x1c, 0xec, 0x80, 0x90, 0xcc, 0x84, 0x71, 0xe4,
+	0xa8, 0x66, 0x5e, 0xe3, 0x73, 0x48, 0x67, 0x97, 0x81, 0xa6, 0x81, 0x36, 0x11, 0xe6, 0xd0, 0x29,
+	0x85, 0x23, 0xdd, 0x00, 0xbd, 0xf4, 0xd5, 0x1f, 0xb8, 0xbd, 0x27, 0xbd, 0xba, 0xda, 0x6b, 0x3c,
+	0x85, 0x6c, 0x66, 0x04, 0x77, 0xe2, 0xcb, 0xa5, 0x23, 0x59, 0x48, 0xec, 0x01, 0x9e, 0xc0, 0x51,
+	0x30, 0x85, 0x40, 0xc8, 0xd4, 0x01, 0x1d, 0xc2, 0xd3, 0xdd, 0x2d, 0x6a, 0x03, 0x26, 0x63, 0xe8,
+	0x6d, 0x7d, 0x44, 0x80, 0xb4, 0xfc, 0xc8, 0xe6, 0xef, 0xde, 0xe7, 0x4f, 0xb0, 0x0b, 0x9d, 0xcf,
+	0x6f, 0x59, 0x1e, 0x79, 0xf1, 0x69, 0xc6, 0xf2, 0xf8, 0xfc, 0x77, 0x04, 0xe0, 0x0d, 0x14, 0x66,
+	0x53, 0xdd, 0x0a, 0x64, 0x90, 0xd6, 0x5e, 0xe2, 0x49, 0x73, 0x8c, 0xd6, 0xe1, 0x47, 0xcf, 0xfe,
+	0xa3, 0xf5, 0xff, 0xe8, 0xcb, 0x1f, 0x7f, 0xfe, 0xfe, 0x8a, 0x8f, 0xe9, 0xa0, 0xd8, 0xbc, 0x2a,
+	0x36, 0x5a, 0x16, 0x36, 0xe4, 0x2f, 0xa2, 0x09, 0x5e, 0x41, 0xe2, 0x47, 0x43, 0x6c, 0x2a, 0x0f,
+	0xde, 0xcc, 0xe8, 0xb8, 0xc5, 0x9a, 0x5e, 0x2f, 0x42, 0xaf, 0x21, 0xed, 0x6f, 0x7b, 0x55, 0xb2,
+	0x72, 0x17, 0xd1, 0x64, 0x99, 0x86, 0xf7, 0xf8, 0xfa, 0x5f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x93,
+	0x52, 0x74, 0x1b, 0xbe, 0x02, 0x00, 0x00,
 }

+ 1 - 0
pb/vpn.proto

@@ -15,6 +15,7 @@ message VPNInitRequest {
   string Hostname = 1;
   string Port = 2;
   VPNProto Protopref = 3;
+  string IPBlock = 4;
 }
 
 service VPNService {

+ 3 - 0
pb/vpn.swagger.json

@@ -80,6 +80,9 @@
         },
         "Protopref": {
           "$ref": "#/definitions/pbVPNProto"
+        },
+        "IPBlock": {
+          "type": "string"
         }
       }
     },

+ 2 - 2
template/server.conf.tmpl

@@ -248,8 +248,8 @@ comp-lzo
 #
 # You can uncomment this out on
 # non-Windows systems.
-user nobody
-group nobody
+user openvpn
+group openvpn
 
 # The persist options will try to avoid
 # accessing certain resources on restart

+ 31 - 19
user.go

@@ -170,7 +170,6 @@ func (u *DBUser) Update(password string, nogw bool, hostid uint32) error {
 
 	u.NoGW = nogw
 	u.HostID = hostid
-	db.Save(u)
 
 	if hostid != 0 {
 		server, err := GetServerInstance()
@@ -192,6 +191,7 @@ func (u *DBUser) Update(password string, nogw bool, hostid uint32) error {
 			return fmt.Errorf("ip %s is already allocated", ip)
 		}
 	}
+	db.Save(u)
 
 	err := Emit()
 	if err != nil {
@@ -244,6 +244,8 @@ func (u *DBUser) ResetPassword(password string) error {
 //
 // This is often used to sign users when the current CA is changed while there are
 // still  existing users in the database.
+//
+// Also it can be used when a user cert is expired or user's private key stolen, missing etc.
 func (u *DBUser) Renew() error {
 	if !IsInitialized() {
 		return fmt.Errorf("you first need to create server")
@@ -301,37 +303,47 @@ func (u *DBUser) GetCreatedAt() string {
 func (u *DBUser) getIP() net.IP {
 	users := getNonStaticHostUsers()
 	staticHostIDs := getStaticHostIDs()
-	mask := net.IPMask(net.ParseIP(_DefaultServerNetMask).To4())
-	network := net.ParseIP(_DefaultServerNetwork).To4().Mask(mask)
+	server, err := GetServerInstance()
+	if err != nil {
+		logrus.Panicf("can not get server instance: %v", err)
+	}
+	mask := net.IPMask(net.ParseIP(server.Mask).To4())
+	network := net.ParseIP(server.Net).To4().Mask(mask)
 
-	// Host is static?
+	// If the user has static ip address, return it immediately.
 	if u.HostID != 0 {
-		// Host is really static?
-		if hostIDsContains(staticHostIDs, u.HostID) {
-			return HostID2IP(u.HostID)
-		}
-		return nil
+		return HostID2IP(u.HostID)
 	}
 
-	// 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)
-			}
+	// Calculate dynamic ip addresses from a deterministic address pool.
+	freeHostID := 0
+	for _, user := range users {
+		// Skip, if user is supposed to have static ip.
+		if user.HostID != 0 {
+			continue
+		}
+
+		// Try the next available host id.
+		hostID := IP2HostID(network) + uint32(freeHostID)
+		for hostIDsContains(staticHostIDs, hostID+2) {
+			freeHostID++ // Increase the host id and try again until it is available.
+			hostID = IP2HostID(network) + uint32(freeHostID)
 		}
 		if user.ID == u.ID {
-			return HostID2IP(hostID)
+			return HostID2IP(hostID + 2)
 		}
+		freeHostID++
 	}
 	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())
+	server, err := GetServerInstance()
+	if err != nil {
+		logrus.Panicf("can not get user ipnet: %v", err)
+	}
+	mask := net.IPMask(net.ParseIP(server.Mask).To4())
 
 	ipn := net.IPNet{
 		IP:   u.getIP(),

+ 27 - 18
user_test.go

@@ -13,7 +13,7 @@ func TestCreateNewUser(t *testing.T) {
 	// Initialize:
 	ovpm.SetupDB("sqlite3", ":memory:")
 	defer ovpm.CeaseDB()
-	ovpm.Init("localhost", "", ovpm.UDPProto)
+	ovpm.Init("localhost", "", ovpm.UDPProto, "")
 	server, _ := ovpm.GetServerInstance()
 
 	// Prepare:
@@ -75,13 +75,21 @@ func TestCreateNewUser(t *testing.T) {
 	if user.NoGW != noGW {
 		t.Fatalf("user.NoGW is expected to be %t but it's %t instead", noGW, user.NoGW)
 	}
+
+	// Try to create a user with an invalid static ip.
+	user = nil
+	_, err = ovpm.CreateNewUser("staticuser", password, noGW, ovpm.IP2HostID(net.ParseIP("8.8.8.8").To4()))
+	if err == nil {
+		t.Fatalf("user creation expected to err but it didn't")
+	}
+
 }
 
 func TestUserUpdate(t *testing.T) {
 	// Initialize:
 	ovpm.SetupDB("sqlite3", ":memory:")
 	defer ovpm.CeaseDB()
-	ovpm.Init("localhost", "", ovpm.UDPProto)
+	ovpm.Init("localhost", "", ovpm.UDPProto, "")
 
 	// Prepare:
 	username := "testUser"
@@ -97,16 +105,18 @@ func TestUserUpdate(t *testing.T) {
 	var updatetests = []struct {
 		password string
 		noGW     bool
+		hostid   uint32
 		ok       bool
 	}{
-		{"testpw", false, true},
-		{"123", false, true},
-		{"123", false, true},
-		{"", true, true},
+		{"testpw", false, 0, true},
+		{"123", false, 0, true},
+		{"123", false, 0, true},
+		{"", true, 0, true},
+		{"", true, ovpm.IP2HostID(net.ParseIP("10.10.10.10").To4()), false}, // Invalid static address.
 	}
 
 	for _, tt := range updatetests {
-		err := user.Update(tt.password, tt.noGW, 0)
+		err := user.Update(tt.password, tt.noGW, tt.hostid)
 		if (err == nil) != tt.ok {
 			t.Errorf("user is expected to be able to update but it gave us this error instead: %v", err)
 		}
@@ -117,7 +127,7 @@ func TestUserPasswordCorrect(t *testing.T) {
 	// Initialize:
 	ovpm.SetupDB("sqlite3", ":memory:")
 	defer ovpm.CeaseDB()
-	ovpm.Init("localhost", "", ovpm.UDPProto)
+	ovpm.Init("localhost", "", ovpm.UDPProto, "")
 
 	// Prepare:
 	initialPassword := "g00dp@ssW0rd9"
@@ -134,7 +144,7 @@ func TestUserPasswordReset(t *testing.T) {
 	// Initialize:
 	ovpm.SetupDB("sqlite3", ":memory:")
 	defer ovpm.CeaseDB()
-	ovpm.Init("localhost", "", ovpm.UDPProto)
+	ovpm.Init("localhost", "", ovpm.UDPProto, "")
 
 	// Prepare:
 	initialPassword := "g00dp@ssW0rd9"
@@ -161,7 +171,7 @@ func TestUserDelete(t *testing.T) {
 	// Initialize:
 	ovpm.SetupDB("sqlite3", ":memory:")
 	defer ovpm.CeaseDB()
-	ovpm.Init("localhost", "", ovpm.UDPProto)
+	ovpm.Init("localhost", "", ovpm.UDPProto, "")
 
 	// Prepare:
 	username := "testUser"
@@ -199,7 +209,7 @@ func TestUserGet(t *testing.T) {
 	// Initialize:
 	ovpm.SetupDB("sqlite3", ":memory:")
 	defer ovpm.CeaseDB()
-	ovpm.Init("localhost", "", ovpm.UDPProto)
+	ovpm.Init("localhost", "", ovpm.UDPProto, "")
 
 	// Prepare:
 	username := "testUser"
@@ -223,7 +233,7 @@ func TestUserGetAll(t *testing.T) {
 	// Initialize:
 	ovpm.SetupDB("sqlite3", ":memory:")
 	defer ovpm.CeaseDB()
-	ovpm.Init("localhost", "", ovpm.UDPProto)
+	ovpm.Init("localhost", "", ovpm.UDPProto, "")
 	count := 5
 
 	// Prepare:
@@ -261,14 +271,14 @@ func TestUserRenew(t *testing.T) {
 	// Initialize:
 	ovpm.SetupDB("sqlite3", ":memory:")
 	defer ovpm.CeaseDB()
-	ovpm.Init("localhost", "", ovpm.UDPProto)
+	ovpm.Init("localhost", "", ovpm.UDPProto, "")
 
 	// Prepare:
 	user, _ := ovpm.CreateNewUser("user", "1234", false, 0)
 
 	// Test:
 	// Re initialize the server.
-	ovpm.Init("example.com", "3333", ovpm.UDPProto) // This causes implicit Renew() on every user in the system.
+	ovpm.Init("example.com", "3333", ovpm.UDPProto, "") // This causes implicit Renew() on every user in the system.
 
 	// Fetch user back.
 	fetchedUser, _ := ovpm.GetUser(user.GetUsername())
@@ -283,7 +293,7 @@ func TestUserIPAllocator(t *testing.T) {
 	// Initialize:
 	ovpm.SetupDB("sqlite3", ":memory:")
 	defer ovpm.CeaseDB()
-	ovpm.Init("localhost", "", ovpm.UDPProto)
+	ovpm.Init("localhost", "", ovpm.UDPProto, "")
 
 	// Prepare:
 
@@ -299,9 +309,8 @@ func TestUserIPAllocator(t *testing.T) {
 		{"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},
+		{"user7", true, 0, "10.9.0.6/24", true},
 	}
 	for _, tt := range iptests {
 		user, err := ovpm.CreateNewUser(tt.username, "pass", tt.gw, tt.hostid)
@@ -310,7 +319,7 @@ func TestUserIPAllocator(t *testing.T) {
 		}
 		if user != nil {
 			if user.GetIPNet() != tt.expectedIP {
-				t.Fatalf("%s is expected to be %s", user.GetIPNet(), tt.expectedIP)
+				t.Fatalf("user %s ip %s is expected to be %s", user.GetUsername(), user.GetIPNet(), tt.expectedIP)
 			}
 		}
 	}

+ 59 - 9
vpn.go

@@ -69,10 +69,17 @@ type _VPNServerConfig struct {
 	Proto        string
 }
 
-// Init regenerates keys and certs for a Root CA, and saves them in the database.
+// Init regenerates keys and certs for a Root CA, gets initial settings for the VPN server
+// and saves them in the database.
 //
-// proto can be either "udp" or "tcp" and if it's "" it defaults to "udp".
-func Init(hostname string, port string, proto string) error {
+// 'proto' can be either "udp" or "tcp" and if it's "" it defaults to "udp".
+//
+// 'ipblock' is a IP network in the CIDR form. VPN clients get their IP addresses from this network.
+// It defaults to const 'DefaultVPNNetwork'.
+//
+// Please note that, Init is potentially destructive procedure, it will cause invalidation of
+// existing .ovpn profiles of the current users. So it should be used carefully.
+func Init(hostname string, port string, proto string, ipblock string) error {
 	if port == "" {
 		port = DefaultVPNPort
 	}
@@ -88,6 +95,33 @@ func Init(hostname string, port string, proto string) error {
 		return fmt.Errorf("validation error: proto:`%s` should be either 'tcp' or 'udp'", proto)
 	}
 
+	// vpn network to use.
+	var ipnet *net.IPNet
+
+	// If user didn't specify, pick the vpn network from defaults.
+	if ipblock == "" {
+		var err error
+		_, ipnet, err = net.ParseCIDR(DefaultVPNNetwork)
+		if err != nil {
+			return fmt.Errorf("can not parse CIDR %s: %v", DefaultVPNNetwork, err)
+		}
+	}
+
+	// Check if the user specified vpn network is valid.
+	if ipblock != "" && !govalidator.IsCIDR(ipblock) {
+		return fmt.Errorf("validation error: ipblock:`%s` should be a CIDR network", ipblock)
+	}
+
+	// Use user specified vpn network.
+	if ipblock != "" {
+		var err error
+		_, ipnet, err = net.ParseCIDR(ipblock)
+		if err != nil {
+			return fmt.Errorf("can parse ipblock: %s", err)
+
+		}
+	}
+
 	if !govalidator.IsNumeric(port) {
 		return fmt.Errorf("validation error: port:`%s` should be numeric", port)
 	}
@@ -113,6 +147,7 @@ func Init(hostname string, port string, proto string) error {
 	if err != nil {
 		return fmt.Errorf("can not create server cert creds: %s", err)
 	}
+
 	serialNumber := uuid.New().String()
 	serverInstance := DBServer{
 		Name: serverName,
@@ -125,8 +160,8 @@ func Init(hostname string, port string, proto string) error {
 		Key:          srv.Key,
 		CACert:       ca.Cert,
 		CAKey:        ca.Key,
-		Net:          _DefaultServerNetwork,
-		Mask:         _DefaultServerNetMask,
+		Net:          ipnet.IP.To4().String(),
+		Mask:         net.IP(ipnet.Mask).To4().String(),
 	}
 
 	db.Create(&serverInstance)
@@ -375,6 +410,11 @@ func emitToFile(path, content string, mode uint) error {
 }
 
 func emitServerConf() error {
+	dbServer, err := GetServerInstance()
+	if err != nil {
+		return fmt.Errorf("can not get server instance: %v", err)
+	}
+
 	serverInstance, err := GetServerInstance()
 	if err != nil {
 		return fmt.Errorf("can not retrieve server: %v", err)
@@ -384,6 +424,11 @@ func emitServerConf() error {
 		port = serverInstance.Port
 	}
 
+	proto := DefaultVPNProto
+	if serverInstance.Proto != "" {
+		proto = serverInstance.Proto
+	}
+
 	var result bytes.Buffer
 
 	server := _VPNServerConfig{
@@ -394,10 +439,10 @@ func emitServerConf() error {
 		CCDPath:      _DefaultVPNCCDPath,
 		CRLPath:      _DefaultCRLPath,
 		DHParamsPath: _DefaultDHParamsPath,
-		Net:          _DefaultServerNetwork,
-		Mask:         _DefaultServerNetMask,
+		Net:          dbServer.Net,
+		Mask:         dbServer.Mask,
 		Port:         port,
-		Proto:        serverInstance.Proto,
+		Proto:        proto,
 	}
 	data, err := bindata.Asset("template/server.conf.tmpl")
 	if err != nil {
@@ -526,6 +571,11 @@ func emitCCD() error {
 			}
 		}
 	}
+	server, err := GetServerInstance()
+	if err != nil {
+		return fmt.Errorf("can not get server instance: %v", err)
+	}
+
 	// Render ccd templates for the users.
 	for _, user := range users {
 		var associatedRoutes [][3]string
@@ -550,7 +600,7 @@ func emitCCD() error {
 			NetMask    string
 			Routes     [][3]string // [0] is IP, [1] is Netmask, [2] is Via
 			RedirectGW bool
-		}{IP: user.getIP().String(), NetMask: _DefaultServerNetMask, Routes: associatedRoutes, RedirectGW: !user.NoGW}
+		}{IP: user.getIP().String(), NetMask: server.Mask, Routes: associatedRoutes, RedirectGW: !user.NoGW}
 
 		data, err := bindata.Asset("template/ccd.file.tmpl")
 		if err != nil {

+ 12 - 12
vpn_test.go

@@ -35,13 +35,13 @@ func TestVPNInit(t *testing.T) {
 	}
 
 	// Wrongfully initialize server.
-	err := Init("localhost", "asdf", UDPProto)
+	err := Init("localhost", "asdf", UDPProto, "")
 	if err == nil {
 		t.Fatalf("error is expected to be not nil but it's nil instead")
 	}
 
 	// Initialize the server.
-	Init("localhost", "", UDPProto)
+	Init("localhost", "", UDPProto, "")
 
 	// Check database if the database has no server.
 	var server2 DBServer
@@ -61,7 +61,7 @@ func TestVPNDeinit(t *testing.T) {
 
 	// Prepare:
 	// Initialize the server.
-	Init("localhost", "", UDPProto)
+	Init("localhost", "", UDPProto, "")
 	u, err := CreateNewUser("user", "p", false, 0)
 	if err != nil {
 		t.Fatal(err)
@@ -122,7 +122,7 @@ func TestVPNIsInitialized(t *testing.T) {
 	}
 
 	// Initialize the server.
-	Init("localhost", "", UDPProto)
+	Init("localhost", "", UDPProto, "")
 
 	// Isn't initialized?
 	if !IsInitialized() {
@@ -152,7 +152,7 @@ func TestVPNGetServerInstance(t *testing.T) {
 	}
 
 	// Initialize server.
-	Init("localhost", "", UDPProto)
+	Init("localhost", "", UDPProto, "")
 
 	server, err = GetServerInstance()
 
@@ -172,7 +172,7 @@ func TestVPNDumpsClientConfig(t *testing.T) {
 	setupTestCase()
 	SetupDB("sqlite3", ":memory:")
 	defer CeaseDB()
-	Init("localhost", "", UDPProto)
+	Init("localhost", "", UDPProto, "")
 
 	// Prepare:
 	user, _ := CreateNewUser("user", "password", false, 0)
@@ -194,7 +194,7 @@ func TestVPNDumpClientConfig(t *testing.T) {
 	setupTestCase()
 	SetupDB("sqlite3", ":memory:")
 	defer CeaseDB()
-	Init("localhost", "", UDPProto)
+	Init("localhost", "", UDPProto, "")
 
 	// Prepare:
 	noGW := false
@@ -262,7 +262,7 @@ func TestVPNGetSystemCA(t *testing.T) {
 	}
 
 	// Initialize system.
-	Init("localhost", "", UDPProto)
+	Init("localhost", "", UDPProto, "")
 
 	ca, err = GetSystemCA()
 	if err != nil {
@@ -302,7 +302,7 @@ func TestVPNStartVPNProc(t *testing.T) {
 	}
 
 	// Initialize OVPM server.
-	Init("localhost", "", UDPProto)
+	Init("localhost", "", UDPProto, "")
 
 	// Call start again..
 	StartVPNProc()
@@ -318,7 +318,7 @@ func TestVPNStopVPNProc(t *testing.T) {
 	setupTestCase()
 	SetupDB("sqlite3", ":memory:")
 	defer CeaseDB()
-	Init("localhost", "", UDPProto)
+	Init("localhost", "", UDPProto, "")
 
 	// Prepare:
 	vpnProc.Start()
@@ -342,7 +342,7 @@ func TestVPNRestartVPNProc(t *testing.T) {
 	// Init:
 	SetupDB("sqlite3", ":memory:")
 	defer CeaseDB()
-	Init("localhost", "", UDPProto)
+	Init("localhost", "", UDPProto, "")
 
 	// Prepare:
 
@@ -371,7 +371,7 @@ func TestVPNEmit(t *testing.T) {
 	setupTestCase()
 	SetupDB("sqlite3", ":memory:")
 	defer CeaseDB()
-	Init("localhost", "", UDPProto)
+	Init("localhost", "", UDPProto, "")
 
 	// Prepare:
 

Деякі файли не було показано, через те що забагато файлів було змінено