From 309f868a2113c9cf1ba193f744ff135808829eb0 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Mon, 2 Aug 2021 20:06:26 +0100 Subject: [PATCH 1/8] Make IP prefix configurable This commit makes the IP prefix used to generate addresses configurable to users. This can be useful if you would like to use a smaller range or if your current setup is overlapping with the current range. The current range is left as a default --- app.go | 2 ++ app_test.go | 5 ++++- cmd/headscale/cli/utils.go | 4 ++++ utils.go | 9 +++++---- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/app.go b/app.go index fa9b011b..e3978212 100644 --- a/app.go +++ b/app.go @@ -13,6 +13,7 @@ import ( "github.com/gin-gonic/gin" "golang.org/x/crypto/acme/autocert" "gorm.io/gorm" + "inet.af/netaddr" "tailscale.com/tailcfg" "tailscale.com/types/wgkey" ) @@ -24,6 +25,7 @@ type Config struct { PrivateKeyPath string DerpMap *tailcfg.DERPMap EphemeralNodeInactivityTimeout time.Duration + IPPrefix netaddr.IPPrefix DBtype string DBpath string diff --git a/app_test.go b/app_test.go index ad633334..ff3755e9 100644 --- a/app_test.go +++ b/app_test.go @@ -6,6 +6,7 @@ import ( "testing" "gopkg.in/check.v1" + "inet.af/netaddr" ) func Test(t *testing.T) { @@ -36,7 +37,9 @@ func (s *Suite) ResetDB(c *check.C) { if err != nil { c.Fatal(err) } - cfg := Config{} + cfg := Config{ + IPPrefix: netaddr.MustParseIPPrefix("127.0.0.1/32"), + } h = Headscale{ cfg: cfg, diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index 5e47d157..1c259c74 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -14,6 +14,7 @@ import ( "github.com/juanfont/headscale" "github.com/spf13/viper" "gopkg.in/yaml.v2" + "inet.af/netaddr" "tailscale.com/tailcfg" ) @@ -36,6 +37,8 @@ func LoadConfig(path string) error { viper.SetDefault("tls_letsencrypt_cache_dir", "/var/www/.cache") viper.SetDefault("tls_letsencrypt_challenge_type", "HTTP-01") + viper.SetDefault("ip_prefix", "100.64.0.0/10") + err := viper.ReadInConfig() if err != nil { return fmt.Errorf("Fatal error reading config file: %s \n", err) @@ -97,6 +100,7 @@ func getHeadscaleApp() (*headscale.Headscale, error) { Addr: viper.GetString("listen_addr"), PrivateKeyPath: absPath(viper.GetString("private_key_path")), DerpMap: derpMap, + IPPrefix: netaddr.MustParseIPPrefix(viper.GetString("ip_prefix")), EphemeralNodeInactivityTimeout: viper.GetDuration("ephemeral_node_inactivity_timeout"), diff --git a/utils.go b/utils.go index f21063b0..1da25084 100644 --- a/utils.go +++ b/utils.go @@ -19,6 +19,7 @@ import ( "golang.org/x/crypto/nacl/box" "gorm.io/gorm" + "inet.af/netaddr" "tailscale.com/types/wgkey" ) @@ -80,7 +81,7 @@ func encodeMsg(b []byte, pubKey *wgkey.Key, privKey *wgkey.Private) ([]byte, err func (h *Headscale) getAvailableIP() (*net.IP, error) { i := 0 for { - ip, err := getRandomIP() + ip, err := getRandomIP(h.cfg.IPPrefix) if err != nil { return nil, err } @@ -93,12 +94,12 @@ func (h *Headscale) getAvailableIP() (*net.IP, error) { break } } - return nil, errors.New("Could not find an available IP address in 100.64.0.0/10") + return nil, errors.New(fmt.Sprintf("Could not find an available IP address in %s", h.cfg.IPPrefix.String())) } -func getRandomIP() (*net.IP, error) { +func getRandomIP(ipPrefix netaddr.IPPrefix) (*net.IP, error) { mathrand.Seed(time.Now().Unix()) - ipo, ipnet, err := net.ParseCIDR("100.64.0.0/10") + ipo, ipnet, err := net.ParseCIDR(ipPrefix.String()) if err == nil { ip := ipo.To4() // fmt.Println("In Randomize IPAddr: IP ", ip, " IPNET: ", ipnet) From b5841c8a8b088338db8cbda474a85243452c9433 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Mon, 2 Aug 2021 21:57:45 +0100 Subject: [PATCH 2/8] Rework getAvailableIp This commit reworks getAvailableIp with a "simpler" version that will look for the first available IP address in our IP Prefix. There is a couple of ideas behind this: * Make the host IPs reasonably predictable and in within similar subnets, which should simplify ACLs for subnets * The code is not random, but deterministic so we can have tests * The code is a bit more understandable (no bit shift magic) --- app_test.go | 2 +- cli_test.go | 1 + utils.go | 107 +++++++++++++++++++++++++++++--------------------- utils_test.go | 105 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 170 insertions(+), 45 deletions(-) create mode 100644 utils_test.go diff --git a/app_test.go b/app_test.go index ff3755e9..5e53f1cc 100644 --- a/app_test.go +++ b/app_test.go @@ -38,7 +38,7 @@ func (s *Suite) ResetDB(c *check.C) { c.Fatal(err) } cfg := Config{ - IPPrefix: netaddr.MustParseIPPrefix("127.0.0.1/32"), + IPPrefix: netaddr.MustParseIPPrefix("10.27.0.0/23"), } h = Headscale{ diff --git a/cli_test.go b/cli_test.go index 9616b4a2..528a115e 100644 --- a/cli_test.go +++ b/cli_test.go @@ -15,6 +15,7 @@ func (s *Suite) TestRegisterMachine(c *check.C) { DiscoKey: "faa", Name: "testmachine", NamespaceID: n.ID, + IPAddress: "10.0.0.1", } h.db.Save(&m) diff --git a/utils.go b/utils.go index 1da25084..404e3823 100644 --- a/utils.go +++ b/utils.go @@ -7,18 +7,11 @@ package headscale import ( "crypto/rand" - "encoding/binary" "encoding/json" - "errors" "fmt" "io" - "net" - "time" - - mathrand "math/rand" "golang.org/x/crypto/nacl/box" - "gorm.io/gorm" "inet.af/netaddr" "tailscale.com/types/wgkey" ) @@ -78,47 +71,73 @@ func encodeMsg(b []byte, pubKey *wgkey.Key, privKey *wgkey.Private) ([]byte, err return msg, nil } -func (h *Headscale) getAvailableIP() (*net.IP, error) { - i := 0 +func (h *Headscale) getAvailableIP() (*netaddr.IP, error) { + ipPrefix := h.cfg.IPPrefix + + usedIps, err := h.getUsedIPs() + if err != nil { + return nil, err + } + + // for _, ip := range usedIps { + // nextIP := ip.Next() + + // if !containsIPs(usedIps, nextIP) && ipPrefix.Contains(nextIP) { + // return &nextIP, nil + // } + // } + + // // If there are no IPs in use, we are starting fresh and + // // can issue IPs from the beginning of the prefix. + // ip := ipPrefix.IP() + // return &ip, nil + + // return nil, fmt.Errorf("failed to find any available IP in %s", ipPrefix) + + // Get the first IP in our prefix + ip := ipPrefix.IP() + for { - ip, err := getRandomIP(h.cfg.IPPrefix) + if !ipPrefix.Contains(ip) { + return nil, fmt.Errorf("could not find any suitable IP in %s", ipPrefix) + } + + if ip.IsZero() && + ip.IsLoopback() { + continue + } + + if !containsIPs(usedIps, ip) { + return &ip, nil + } + + ip = ip.Next() + } +} + +func (h *Headscale) getUsedIPs() ([]netaddr.IP, error) { + var addresses []string + h.db.Model(&Machine{}).Pluck("ip_address", &addresses) + + ips := make([]netaddr.IP, len(addresses)) + for index, addr := range addresses { + ip, err := netaddr.ParseIP(addr) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to parse ip from database, %w", err) } - m := Machine{} - if result := h.db.First(&m, "ip_address = ?", ip.String()); errors.Is(result.Error, gorm.ErrRecordNotFound) { - return ip, nil - } - i++ - if i == 100 { // really random number - break - } - } - return nil, errors.New(fmt.Sprintf("Could not find an available IP address in %s", h.cfg.IPPrefix.String())) -} -func getRandomIP(ipPrefix netaddr.IPPrefix) (*net.IP, error) { - mathrand.Seed(time.Now().Unix()) - ipo, ipnet, err := net.ParseCIDR(ipPrefix.String()) - if err == nil { - ip := ipo.To4() - // fmt.Println("In Randomize IPAddr: IP ", ip, " IPNET: ", ipnet) - // fmt.Println("Final address is ", ip) - // fmt.Println("Broadcast address is ", ipb) - // fmt.Println("Network address is ", ipn) - r := mathrand.Uint32() - ipRaw := make([]byte, 4) - binary.LittleEndian.PutUint32(ipRaw, r) - // ipRaw[3] = 254 - // fmt.Println("ipRaw is ", ipRaw) - for i, v := range ipRaw { - // fmt.Println("IP Before: ", ip[i], " v is ", v, " Mask is: ", ipnet.Mask[i]) - ip[i] = ip[i] + (v &^ ipnet.Mask[i]) - // fmt.Println("IP After: ", ip[i]) - } - // fmt.Println("FINAL IP: ", ip.String()) - return &ip, nil + ips[index] = ip } - return nil, err + return ips, nil +} + +func containsIPs(ips []netaddr.IP, ip netaddr.IP) bool { + for _, v := range ips { + if v == ip { + return true + } + } + + return false } diff --git a/utils_test.go b/utils_test.go new file mode 100644 index 00000000..471b8220 --- /dev/null +++ b/utils_test.go @@ -0,0 +1,105 @@ +package headscale + +import ( + "gopkg.in/check.v1" + "inet.af/netaddr" +) + +func (s *Suite) TestGetAvailableIp(c *check.C) { + ip, err := h.getAvailableIP() + + c.Assert(err, check.IsNil) + + expected := netaddr.MustParseIP("10.27.0.0") + + c.Assert(ip.String(), check.Equals, expected.String()) +} + +func (s *Suite) TestGetUsedIps(c *check.C) { + ip, err := h.getAvailableIP() + c.Assert(err, check.IsNil) + + n, err := h.CreateNamespace("test_ip") + c.Assert(err, check.IsNil) + + pak, err := h.CreatePreAuthKey(n.Name, false, false, nil) + c.Assert(err, check.IsNil) + + _, err = h.GetMachine("test", "testmachine") + c.Assert(err, check.NotNil) + + m := Machine{ + ID: 0, + MachineKey: "foo", + NodeKey: "bar", + DiscoKey: "faa", + Name: "testmachine", + NamespaceID: n.ID, + Registered: true, + RegisterMethod: "authKey", + AuthKeyID: uint(pak.ID), + IPAddress: ip.String(), + } + h.db.Save(&m) + + ips, err := h.getUsedIPs() + + c.Assert(err, check.IsNil) + + expected := netaddr.MustParseIP("10.27.0.0") + + c.Assert(ips[0], check.Equals, expected) +} + +func (s *Suite) TestGetMultiIp(c *check.C) { + n, err := h.CreateNamespace("test-ip-multi") + c.Assert(err, check.IsNil) + + for i := 1; i <= 350; i++ { + ip, err := h.getAvailableIP() + c.Assert(err, check.IsNil) + + pak, err := h.CreatePreAuthKey(n.Name, false, false, nil) + c.Assert(err, check.IsNil) + + _, err = h.GetMachine("test", "testmachine") + c.Assert(err, check.NotNil) + + m := Machine{ + ID: 0, + MachineKey: "foo", + NodeKey: "bar", + DiscoKey: "faa", + Name: "testmachine", + NamespaceID: n.ID, + Registered: true, + RegisterMethod: "authKey", + AuthKeyID: uint(pak.ID), + IPAddress: ip.String(), + } + h.db.Save(&m) + } + + ips, err := h.getUsedIPs() + + c.Assert(err, check.IsNil) + + c.Assert(len(ips), check.Equals, 350) + + c.Assert(ips[0], check.Equals, netaddr.MustParseIP("10.27.0.0")) + c.Assert(ips[9], check.Equals, netaddr.MustParseIP("10.27.0.9")) + c.Assert(ips[300], check.Equals, netaddr.MustParseIP("10.27.1.44")) + + expectedNextIP := netaddr.MustParseIP("10.27.1.94") + nextIP, err := h.getAvailableIP() + c.Assert(err, check.IsNil) + + c.Assert(nextIP.String(), check.Equals, expectedNextIP.String()) + + // If we call get Available again, we should receive + // the same IP, as it has not been reserved. + nextIP2, err := h.getAvailableIP() + c.Assert(err, check.IsNil) + + c.Assert(nextIP2.String(), check.Equals, expectedNextIP.String()) +} From 9f85efffd5fc2b3d8c9cd7057c8dfab4a691d3c5 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Mon, 2 Aug 2021 22:06:15 +0100 Subject: [PATCH 3/8] Update readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5f90c831..16448534 100644 --- a/README.md +++ b/README.md @@ -113,9 +113,10 @@ Headscale's configuration file is named `config.json` or `config.yaml`. Headscal ``` "server_url": "http://192.168.1.12:8080", "listen_addr": "0.0.0.0:8080", + "ip_prefix": "100.64.0.0/10" ``` -`server_url` is the external URL via which Headscale is reachable. `listen_addr` is the IP address and port the Headscale program should listen on. +`server_url` is the external URL via which Headscale is reachable. `listen_addr` is the IP address and port the Headscale program should listen on. `ip_prefix` is the IP prefix (range) in which IP addresses for nodes will be allocated. ``` "private_key_path": "private.key", From 95de823b7241eec5ae8d763414d86cab7b1dfcf0 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Mon, 2 Aug 2021 22:39:18 +0100 Subject: [PATCH 4/8] Add test to ensure we can read back ips --- utils_test.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/utils_test.go b/utils_test.go index 471b8220..439fdc19 100644 --- a/utils_test.go +++ b/utils_test.go @@ -49,6 +49,11 @@ func (s *Suite) TestGetUsedIps(c *check.C) { expected := netaddr.MustParseIP("10.27.0.0") c.Assert(ips[0], check.Equals, expected) + + m1, err := h.GetMachineByID(0) + c.Assert(err, check.IsNil) + + c.Assert(m1.IPAddress, check.Equals, expected.String()) } func (s *Suite) TestGetMultiIp(c *check.C) { @@ -66,7 +71,7 @@ func (s *Suite) TestGetMultiIp(c *check.C) { c.Assert(err, check.NotNil) m := Machine{ - ID: 0, + ID: uint64(i), MachineKey: "foo", NodeKey: "bar", DiscoKey: "faa", @@ -90,6 +95,15 @@ func (s *Suite) TestGetMultiIp(c *check.C) { c.Assert(ips[9], check.Equals, netaddr.MustParseIP("10.27.0.9")) c.Assert(ips[300], check.Equals, netaddr.MustParseIP("10.27.1.44")) + // Check that we can read back the IPs + m1, err := h.GetMachineByID(1) + c.Assert(err, check.IsNil) + c.Assert(m1.IPAddress, check.Equals, netaddr.MustParseIP("10.27.0.0").String()) + + m50, err := h.GetMachineByID(50) + c.Assert(err, check.IsNil) + c.Assert(m50.IPAddress, check.Equals, netaddr.MustParseIP("10.27.0.49").String()) + expectedNextIP := netaddr.MustParseIP("10.27.1.94") nextIP, err := h.getAvailableIP() c.Assert(err, check.IsNil) From eda6e560c369ea39e286cd1d916bc7e369fd7e03 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Mon, 2 Aug 2021 22:51:50 +0100 Subject: [PATCH 5/8] debug logging --- api.go | 1 + 1 file changed, 1 insertion(+) diff --git a/api.go b/api.go index 088c337f..97ec4d41 100644 --- a/api.go +++ b/api.go @@ -445,6 +445,7 @@ func (h *Headscale) handleAuthKey(c *gin.Context, db *gorm.DB, idKey wgkey.Key, log.Println(err) return } + log.Printf("Assigning %s to %s", ip, m.Name) m.AuthKeyID = uint(pak.ID) m.IPAddress = ip.String() From 73207decfd13b703370d3ea2b57460111363fa4e Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Tue, 3 Aug 2021 07:42:11 +0100 Subject: [PATCH 6/8] Check that IP is set before parsing Machine is saved to db before it is assigned an ip, so we might have empty ip fields coming back. --- utils.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/utils.go b/utils.go index 404e3823..45d44562 100644 --- a/utils.go +++ b/utils.go @@ -121,12 +121,14 @@ func (h *Headscale) getUsedIPs() ([]netaddr.IP, error) { ips := make([]netaddr.IP, len(addresses)) for index, addr := range addresses { - ip, err := netaddr.ParseIP(addr) - if err != nil { - return nil, fmt.Errorf("failed to parse ip from database, %w", err) - } + if addr != "" { + ip, err := netaddr.ParseIP(addr) + if err != nil { + return nil, fmt.Errorf("failed to parse ip from database, %w", err) + } - ips[index] = ip + ips[index] = ip + } } return ips, nil From d3349aa4d13fca1ad6ef28424657d8e6219b1aab Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Tue, 3 Aug 2021 09:26:28 +0100 Subject: [PATCH 7/8] Add test to ensure we can deal with empty ips from database --- utils_test.go | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/utils_test.go b/utils_test.go index 439fdc19..8fbe1375 100644 --- a/utils_test.go +++ b/utils_test.go @@ -117,3 +117,39 @@ func (s *Suite) TestGetMultiIp(c *check.C) { c.Assert(nextIP2.String(), check.Equals, expectedNextIP.String()) } + +func (s *Suite) TestGetAvailableIpMachineWithoutIP(c *check.C) { + ip, err := h.getAvailableIP() + c.Assert(err, check.IsNil) + + expected := netaddr.MustParseIP("10.27.0.0") + + c.Assert(ip.String(), check.Equals, expected.String()) + + n, err := h.CreateNamespace("test_ip") + c.Assert(err, check.IsNil) + + pak, err := h.CreatePreAuthKey(n.Name, false, false, nil) + c.Assert(err, check.IsNil) + + _, err = h.GetMachine("test", "testmachine") + c.Assert(err, check.NotNil) + + m := Machine{ + ID: 0, + MachineKey: "foo", + NodeKey: "bar", + DiscoKey: "faa", + Name: "testmachine", + NamespaceID: n.ID, + Registered: true, + RegisterMethod: "authKey", + AuthKeyID: uint(pak.ID), + } + h.db.Save(&m) + + ip2, err := h.getAvailableIP() + c.Assert(err, check.IsNil) + + c.Assert(ip2.String(), check.Equals, expected.String()) +} From ea615e3a268126ec291e72481f016d3dfd3d7040 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Tue, 3 Aug 2021 10:06:42 +0100 Subject: [PATCH 8/8] Do not issue "network" or "broadcast" addresses (0 or 255) --- utils.go | 26 +++++++++++--------------- utils_test.go | 18 +++++++++--------- 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/utils.go b/utils.go index 45d44562..03dc673f 100644 --- a/utils.go +++ b/utils.go @@ -79,21 +79,6 @@ func (h *Headscale) getAvailableIP() (*netaddr.IP, error) { return nil, err } - // for _, ip := range usedIps { - // nextIP := ip.Next() - - // if !containsIPs(usedIps, nextIP) && ipPrefix.Contains(nextIP) { - // return &nextIP, nil - // } - // } - - // // If there are no IPs in use, we are starting fresh and - // // can issue IPs from the beginning of the prefix. - // ip := ipPrefix.IP() - // return &ip, nil - - // return nil, fmt.Errorf("failed to find any available IP in %s", ipPrefix) - // Get the first IP in our prefix ip := ipPrefix.IP() @@ -102,8 +87,19 @@ func (h *Headscale) getAvailableIP() (*netaddr.IP, error) { return nil, fmt.Errorf("could not find any suitable IP in %s", ipPrefix) } + // Some OS (including Linux) does not like when IPs ends with 0 or 255, which + // is typically called network or broadcast. Lets avoid them and continue + // to look when we get one of those traditionally reserved IPs. + ipRaw := ip.As4() + if ipRaw[3] == 0 || ipRaw[3] == 255 { + ip = ip.Next() + continue + } + if ip.IsZero() && ip.IsLoopback() { + + ip = ip.Next() continue } diff --git a/utils_test.go b/utils_test.go index 8fbe1375..f50cd117 100644 --- a/utils_test.go +++ b/utils_test.go @@ -10,7 +10,7 @@ func (s *Suite) TestGetAvailableIp(c *check.C) { c.Assert(err, check.IsNil) - expected := netaddr.MustParseIP("10.27.0.0") + expected := netaddr.MustParseIP("10.27.0.1") c.Assert(ip.String(), check.Equals, expected.String()) } @@ -46,7 +46,7 @@ func (s *Suite) TestGetUsedIps(c *check.C) { c.Assert(err, check.IsNil) - expected := netaddr.MustParseIP("10.27.0.0") + expected := netaddr.MustParseIP("10.27.0.1") c.Assert(ips[0], check.Equals, expected) @@ -91,20 +91,20 @@ func (s *Suite) TestGetMultiIp(c *check.C) { c.Assert(len(ips), check.Equals, 350) - c.Assert(ips[0], check.Equals, netaddr.MustParseIP("10.27.0.0")) - c.Assert(ips[9], check.Equals, netaddr.MustParseIP("10.27.0.9")) - c.Assert(ips[300], check.Equals, netaddr.MustParseIP("10.27.1.44")) + c.Assert(ips[0], check.Equals, netaddr.MustParseIP("10.27.0.1")) + c.Assert(ips[9], check.Equals, netaddr.MustParseIP("10.27.0.10")) + c.Assert(ips[300], check.Equals, netaddr.MustParseIP("10.27.1.47")) // Check that we can read back the IPs m1, err := h.GetMachineByID(1) c.Assert(err, check.IsNil) - c.Assert(m1.IPAddress, check.Equals, netaddr.MustParseIP("10.27.0.0").String()) + c.Assert(m1.IPAddress, check.Equals, netaddr.MustParseIP("10.27.0.1").String()) m50, err := h.GetMachineByID(50) c.Assert(err, check.IsNil) - c.Assert(m50.IPAddress, check.Equals, netaddr.MustParseIP("10.27.0.49").String()) + c.Assert(m50.IPAddress, check.Equals, netaddr.MustParseIP("10.27.0.50").String()) - expectedNextIP := netaddr.MustParseIP("10.27.1.94") + expectedNextIP := netaddr.MustParseIP("10.27.1.97") nextIP, err := h.getAvailableIP() c.Assert(err, check.IsNil) @@ -122,7 +122,7 @@ func (s *Suite) TestGetAvailableIpMachineWithoutIP(c *check.C) { ip, err := h.getAvailableIP() c.Assert(err, check.IsNil) - expected := netaddr.MustParseIP("10.27.0.0") + expected := netaddr.MustParseIP("10.27.0.1") c.Assert(ip.String(), check.Equals, expected.String())