mirror of
https://github.com/juanfont/headscale.git
synced 2025-08-27 11:16:38 +00:00
Compare commits
15 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
4aad3b7933 | ||
![]() |
6091373b53 | ||
![]() |
3879120967 | ||
![]() |
465669f650 | ||
![]() |
ea615e3a26 | ||
![]() |
d3349aa4d1 | ||
![]() |
73207decfd | ||
![]() |
eda6e560c3 | ||
![]() |
95de823b72 | ||
![]() |
9f85efffd5 | ||
![]() |
b5841c8a8b | ||
![]() |
309f868a21 | ||
![]() |
6c903d4a2f | ||
![]() |
461a893ee4 | ||
![]() |
97f7c90092 |
@@ -24,6 +24,7 @@ Headscale implements this coordination server.
|
|||||||
- [x] Node registration via pre-auth keys (including reusable keys, and ephemeral node support)
|
- [x] Node registration via pre-auth keys (including reusable keys, and ephemeral node support)
|
||||||
- [X] JSON-formatted output
|
- [X] JSON-formatted output
|
||||||
- [X] ACLs
|
- [X] ACLs
|
||||||
|
- [X] Support for alternative IP ranges in the tailnets (default Tailscale's 100.64.0.0/10)
|
||||||
- [ ] Share nodes between ~~users~~ namespaces
|
- [ ] Share nodes between ~~users~~ namespaces
|
||||||
- [ ] DNS
|
- [ ] DNS
|
||||||
|
|
||||||
@@ -113,9 +114,10 @@ Headscale's configuration file is named `config.json` or `config.yaml`. Headscal
|
|||||||
```
|
```
|
||||||
"server_url": "http://192.168.1.12:8080",
|
"server_url": "http://192.168.1.12:8080",
|
||||||
"listen_addr": "0.0.0.0: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 (default 100.64.0.0/10, e.g., 192.168.4.0/24, 10.0.0.0/8)
|
||||||
|
|
||||||
```
|
```
|
||||||
"private_key_path": "private.key",
|
"private_key_path": "private.key",
|
||||||
|
1
api.go
1
api.go
@@ -445,6 +445,7 @@ func (h *Headscale) handleAuthKey(c *gin.Context, db *gorm.DB, idKey wgkey.Key,
|
|||||||
log.Println(err)
|
log.Println(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
log.Printf("Assigning %s to %s", ip, m.Name)
|
||||||
|
|
||||||
m.AuthKeyID = uint(pak.ID)
|
m.AuthKeyID = uint(pak.ID)
|
||||||
m.IPAddress = ip.String()
|
m.IPAddress = ip.String()
|
||||||
|
19
app.go
19
app.go
@@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"golang.org/x/crypto/acme/autocert"
|
"golang.org/x/crypto/acme/autocert"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"inet.af/netaddr"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/types/wgkey"
|
"tailscale.com/types/wgkey"
|
||||||
)
|
)
|
||||||
@@ -24,6 +25,7 @@ type Config struct {
|
|||||||
PrivateKeyPath string
|
PrivateKeyPath string
|
||||||
DerpMap *tailcfg.DERPMap
|
DerpMap *tailcfg.DERPMap
|
||||||
EphemeralNodeInactivityTimeout time.Duration
|
EphemeralNodeInactivityTimeout time.Duration
|
||||||
|
IPPrefix netaddr.IPPrefix
|
||||||
|
|
||||||
DBtype string
|
DBtype string
|
||||||
DBpath string
|
DBpath string
|
||||||
@@ -139,6 +141,20 @@ func (h *Headscale) expireEphemeralNodesWorker() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WatchForKVUpdates checks the KV DB table for requests to perform tailnet upgrades
|
||||||
|
// This is a way to communitate the CLI with the headscale server
|
||||||
|
func (h *Headscale) watchForKVUpdates(milliSeconds int64) {
|
||||||
|
ticker := time.NewTicker(time.Duration(milliSeconds) * time.Millisecond)
|
||||||
|
for range ticker.C {
|
||||||
|
h.watchForKVUpdatesWorker()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Headscale) watchForKVUpdatesWorker() {
|
||||||
|
h.checkForNamespacesPendingUpdates()
|
||||||
|
// more functions will come here in the future
|
||||||
|
}
|
||||||
|
|
||||||
// Serve launches a GIN server with the Headscale API
|
// Serve launches a GIN server with the Headscale API
|
||||||
func (h *Headscale) Serve() error {
|
func (h *Headscale) Serve() error {
|
||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
@@ -147,6 +163,9 @@ func (h *Headscale) Serve() error {
|
|||||||
r.POST("/machine/:id/map", h.PollNetMapHandler)
|
r.POST("/machine/:id/map", h.PollNetMapHandler)
|
||||||
r.POST("/machine/:id", h.RegistrationHandler)
|
r.POST("/machine/:id", h.RegistrationHandler)
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
go h.watchForKVUpdates(5000)
|
||||||
|
|
||||||
if h.cfg.TLSLetsEncryptHostname != "" {
|
if h.cfg.TLSLetsEncryptHostname != "" {
|
||||||
if !strings.HasPrefix(h.cfg.ServerURL, "https://") {
|
if !strings.HasPrefix(h.cfg.ServerURL, "https://") {
|
||||||
log.Println("WARNING: listening with TLS but ServerURL does not start with https://")
|
log.Println("WARNING: listening with TLS but ServerURL does not start with https://")
|
||||||
|
@@ -6,6 +6,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gopkg.in/check.v1"
|
"gopkg.in/check.v1"
|
||||||
|
"inet.af/netaddr"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test(t *testing.T) {
|
func Test(t *testing.T) {
|
||||||
@@ -36,7 +37,9 @@ func (s *Suite) ResetDB(c *check.C) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
c.Fatal(err)
|
c.Fatal(err)
|
||||||
}
|
}
|
||||||
cfg := Config{}
|
cfg := Config{
|
||||||
|
IPPrefix: netaddr.MustParseIPPrefix("10.27.0.0/23"),
|
||||||
|
}
|
||||||
|
|
||||||
h = Headscale{
|
h = Headscale{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
|
@@ -15,6 +15,7 @@ func (s *Suite) TestRegisterMachine(c *check.C) {
|
|||||||
DiscoKey: "faa",
|
DiscoKey: "faa",
|
||||||
Name: "testmachine",
|
Name: "testmachine",
|
||||||
NamespaceID: n.ID,
|
NamespaceID: n.ID,
|
||||||
|
IPAddress: "10.0.0.1",
|
||||||
}
|
}
|
||||||
h.db.Save(&m)
|
h.db.Save(&m)
|
||||||
|
|
||||||
|
@@ -11,7 +11,8 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init () {
|
func init() {
|
||||||
|
rootCmd.AddCommand(nodeCmd)
|
||||||
nodeCmd.PersistentFlags().StringP("namespace", "n", "", "Namespace")
|
nodeCmd.PersistentFlags().StringP("namespace", "n", "", "Namespace")
|
||||||
err := nodeCmd.MarkPersistentFlagRequired("namespace")
|
err := nodeCmd.MarkPersistentFlagRequired("namespace")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/juanfont/headscale"
|
"github.com/juanfont/headscale"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
|
"inet.af/netaddr"
|
||||||
"tailscale.com/tailcfg"
|
"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_cache_dir", "/var/www/.cache")
|
||||||
viper.SetDefault("tls_letsencrypt_challenge_type", "HTTP-01")
|
viper.SetDefault("tls_letsencrypt_challenge_type", "HTTP-01")
|
||||||
|
|
||||||
|
viper.SetDefault("ip_prefix", "100.64.0.0/10")
|
||||||
|
|
||||||
err := viper.ReadInConfig()
|
err := viper.ReadInConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Fatal error reading config file: %s \n", err)
|
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"),
|
Addr: viper.GetString("listen_addr"),
|
||||||
PrivateKeyPath: absPath(viper.GetString("private_key_path")),
|
PrivateKeyPath: absPath(viper.GetString("private_key_path")),
|
||||||
DerpMap: derpMap,
|
DerpMap: derpMap,
|
||||||
|
IPPrefix: netaddr.MustParseIPPrefix(viper.GetString("ip_prefix")),
|
||||||
|
|
||||||
EphemeralNodeInactivityTimeout: viper.GetDuration("ephemeral_node_inactivity_timeout"),
|
EphemeralNodeInactivityTimeout: viper.GetDuration("ephemeral_node_inactivity_timeout"),
|
||||||
|
|
||||||
|
2
db.go
2
db.go
@@ -79,6 +79,7 @@ func (h *Headscale) openDB() (*gorm.DB, error) {
|
|||||||
return db, nil
|
return db, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getValue returns the value for the given key in KV
|
||||||
func (h *Headscale) getValue(key string) (string, error) {
|
func (h *Headscale) getValue(key string) (string, error) {
|
||||||
var row KV
|
var row KV
|
||||||
if result := h.db.First(&row, "key = ?", key); errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
if result := h.db.First(&row, "key = ?", key); errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
@@ -87,6 +88,7 @@ func (h *Headscale) getValue(key string) (string, error) {
|
|||||||
return row.Value, nil
|
return row.Value, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setValue sets value for the given key in KV
|
||||||
func (h *Headscale) setValue(key string, value string) error {
|
func (h *Headscale) setValue(key string, value string) error {
|
||||||
kv := KV{
|
kv := KV{
|
||||||
Key: key,
|
Key: key,
|
||||||
|
@@ -200,19 +200,22 @@ func (h *Headscale) GetMachineByID(id uint64) (*Machine, error) {
|
|||||||
// DeleteMachine softs deletes a Machine from the database
|
// DeleteMachine softs deletes a Machine from the database
|
||||||
func (h *Headscale) DeleteMachine(m *Machine) error {
|
func (h *Headscale) DeleteMachine(m *Machine) error {
|
||||||
m.Registered = false
|
m.Registered = false
|
||||||
|
namespaceID := m.NamespaceID
|
||||||
h.db.Save(&m) // we mark it as unregistered, just in case
|
h.db.Save(&m) // we mark it as unregistered, just in case
|
||||||
if err := h.db.Delete(&m).Error; err != nil {
|
if err := h.db.Delete(&m).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
return h.RequestMapUpdates(namespaceID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HardDeleteMachine hard deletes a Machine from the database
|
// HardDeleteMachine hard deletes a Machine from the database
|
||||||
func (h *Headscale) HardDeleteMachine(m *Machine) error {
|
func (h *Headscale) HardDeleteMachine(m *Machine) error {
|
||||||
|
namespaceID := m.NamespaceID
|
||||||
if err := h.db.Unscoped().Delete(&m).Error; err != nil {
|
if err := h.db.Unscoped().Delete(&m).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return h.RequestMapUpdates(namespaceID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetHostInfo returns a Hostinfo struct for the machine
|
// GetHostInfo returns a Hostinfo struct for the machine
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
package headscale
|
package headscale
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
"gopkg.in/check.v1"
|
"gopkg.in/check.v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -81,6 +83,15 @@ func (s *Suite) TestDeleteMachine(c *check.C) {
|
|||||||
h.db.Save(&m)
|
h.db.Save(&m)
|
||||||
err = h.DeleteMachine(&m)
|
err = h.DeleteMachine(&m)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
v, err := h.getValue("namespaces_pending_updates")
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
names := []string{}
|
||||||
|
err = json.Unmarshal([]byte(v), &names)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
c.Assert(names, check.DeepEquals, []string{n.Name})
|
||||||
|
h.checkForNamespacesPendingUpdates()
|
||||||
|
v, _ = h.getValue("namespaces_pending_updates")
|
||||||
|
c.Assert(v, check.Equals, "")
|
||||||
_, err = h.GetMachine(n.Name, "testmachine")
|
_, err = h.GetMachine(n.Name, "testmachine")
|
||||||
c.Assert(err, check.NotNil)
|
c.Assert(err, check.NotNil)
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
package headscale
|
package headscale
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -103,6 +105,88 @@ func (h *Headscale) SetMachineNamespace(m *Machine, namespaceName string) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RequestMapUpdates signals the KV worker to update the maps for this namespace
|
||||||
|
func (h *Headscale) RequestMapUpdates(namespaceID uint) error {
|
||||||
|
namespace := Namespace{}
|
||||||
|
if err := h.db.First(&namespace, namespaceID).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := h.getValue("namespaces_pending_updates")
|
||||||
|
if err != nil || v == "" {
|
||||||
|
err = h.setValue("namespaces_pending_updates", fmt.Sprintf(`["%s"]`, namespace.Name))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
names := []string{}
|
||||||
|
err = json.Unmarshal([]byte(v), &names)
|
||||||
|
if err != nil {
|
||||||
|
err = h.setValue("namespaces_pending_updates", fmt.Sprintf(`["%s"]`, namespace.Name))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
names = append(names, namespace.Name)
|
||||||
|
data, err := json.Marshal(names)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Could not marshal namespaces_pending_updates: %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return h.setValue("namespaces_pending_updates", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Headscale) checkForNamespacesPendingUpdates() {
|
||||||
|
v, err := h.getValue("namespaces_pending_updates")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if v == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
names := []string{}
|
||||||
|
err = json.Unmarshal([]byte(v), &names)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, name := range names {
|
||||||
|
log.Printf("Sending updates to nodes in namespace %s", name)
|
||||||
|
machines, err := h.ListMachinesInNamespace(name)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, m := range *machines {
|
||||||
|
peers, _ := h.getPeers(m)
|
||||||
|
h.pollMu.Lock()
|
||||||
|
for _, p := range *peers {
|
||||||
|
pUp, ok := h.clientsPolling[uint64(p.ID)]
|
||||||
|
if ok {
|
||||||
|
log.Printf("[%s] Notifying peer %s (%s)", m.Name, p.Name, p.Addresses[0])
|
||||||
|
pUp <- []byte{}
|
||||||
|
} else {
|
||||||
|
log.Printf("[%s] Peer %s does not appear to be polling", m.Name, p.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h.pollMu.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newV, err := h.getValue("namespaces_pending_updates")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if v == newV { // only clear when no changes, so we notified everybody
|
||||||
|
err = h.setValue("namespaces_pending_updates", "")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Could not save to KV: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (n *Namespace) toUser() *tailcfg.User {
|
func (n *Namespace) toUser() *tailcfg.User {
|
||||||
u := tailcfg.User{
|
u := tailcfg.User{
|
||||||
ID: tailcfg.UserID(n.ID),
|
ID: tailcfg.UserID(n.ID),
|
||||||
|
98
utils.go
98
utils.go
@@ -7,18 +7,12 @@ package headscale
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/binary"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
mathrand "math/rand"
|
|
||||||
|
|
||||||
"golang.org/x/crypto/nacl/box"
|
"golang.org/x/crypto/nacl/box"
|
||||||
"gorm.io/gorm"
|
"inet.af/netaddr"
|
||||||
"tailscale.com/types/wgkey"
|
"tailscale.com/types/wgkey"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -77,47 +71,71 @@ func encodeMsg(b []byte, pubKey *wgkey.Key, privKey *wgkey.Private) ([]byte, err
|
|||||||
return msg, nil
|
return msg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Headscale) getAvailableIP() (*net.IP, error) {
|
func (h *Headscale) getAvailableIP() (*netaddr.IP, error) {
|
||||||
i := 0
|
ipPrefix := h.cfg.IPPrefix
|
||||||
|
|
||||||
|
usedIps, err := h.getUsedIPs()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the first IP in our prefix
|
||||||
|
ip := ipPrefix.IP()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
ip, err := getRandomIP()
|
if !ipPrefix.Contains(ip) {
|
||||||
if err != nil {
|
return nil, fmt.Errorf("could not find any suitable IP in %s", ipPrefix)
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
m := Machine{}
|
|
||||||
if result := h.db.First(&m, "ip_address = ?", ip.String()); errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
// Some OS (including Linux) does not like when IPs ends with 0 or 255, which
|
||||||
return ip, nil
|
// 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
|
||||||
}
|
}
|
||||||
i++
|
|
||||||
if i == 100 { // really random number
|
if ip.IsZero() &&
|
||||||
break
|
ip.IsLoopback() {
|
||||||
|
|
||||||
|
ip = ip.Next()
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !containsIPs(usedIps, ip) {
|
||||||
|
return &ip, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ip = ip.Next()
|
||||||
}
|
}
|
||||||
return nil, errors.New("Could not find an available IP address in 100.64.0.0/10")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRandomIP() (*net.IP, error) {
|
func (h *Headscale) getUsedIPs() ([]netaddr.IP, error) {
|
||||||
mathrand.Seed(time.Now().Unix())
|
var addresses []string
|
||||||
ipo, ipnet, err := net.ParseCIDR("100.64.0.0/10")
|
h.db.Model(&Machine{}).Pluck("ip_address", &addresses)
|
||||||
if err == nil {
|
|
||||||
ip := ipo.To4()
|
ips := make([]netaddr.IP, len(addresses))
|
||||||
// fmt.Println("In Randomize IPAddr: IP ", ip, " IPNET: ", ipnet)
|
for index, addr := range addresses {
|
||||||
// fmt.Println("Final address is ", ip)
|
if addr != "" {
|
||||||
// fmt.Println("Broadcast address is ", ipb)
|
ip, err := netaddr.ParseIP(addr)
|
||||||
// fmt.Println("Network address is ", ipn)
|
if err != nil {
|
||||||
r := mathrand.Uint32()
|
return nil, fmt.Errorf("failed to parse ip from database, %w", err)
|
||||||
ipRaw := make([]byte, 4)
|
}
|
||||||
binary.LittleEndian.PutUint32(ipRaw, r)
|
|
||||||
// ipRaw[3] = 254
|
ips[index] = ip
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
155
utils_test.go
Normal file
155
utils_test.go
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
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.1")
|
||||||
|
|
||||||
|
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.1")
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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: uint64(i),
|
||||||
|
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.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.1").String())
|
||||||
|
|
||||||
|
m50, err := h.GetMachineByID(50)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
c.Assert(m50.IPAddress, check.Equals, netaddr.MustParseIP("10.27.0.50").String())
|
||||||
|
|
||||||
|
expectedNextIP := netaddr.MustParseIP("10.27.1.97")
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Suite) TestGetAvailableIpMachineWithoutIP(c *check.C) {
|
||||||
|
ip, err := h.getAvailableIP()
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
expected := netaddr.MustParseIP("10.27.0.1")
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
Reference in New Issue
Block a user