diff --git a/api.go b/api.go index 2ac6f299..d090a2a0 100644 --- a/api.go +++ b/api.go @@ -64,6 +64,7 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { Str("handler", "Registration"). Err(err). Msg("Cannot parse machine key") + machineRegistrations.WithLabelValues("unkown", "web", "error", "unknown").Inc() c.String(http.StatusInternalServerError, "Sad!") return } @@ -74,6 +75,7 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { Str("handler", "Registration"). Err(err). Msg("Cannot decode message") + machineRegistrations.WithLabelValues("unkown", "web", "error", "unknown").Inc() c.String(http.StatusInternalServerError, "Very sad!") return } @@ -94,6 +96,7 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { Str("handler", "Registration"). Err(err). Msg("Could not create row") + machineRegistrations.WithLabelValues("unkown", "web", "error", m.Namespace.Name).Inc() return } } @@ -122,9 +125,11 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { Str("handler", "Registration"). Err(err). Msg("Cannot encode message") + machineRegistrations.WithLabelValues("update", "web", "error", m.Namespace.Name).Inc() c.String(http.StatusInternalServerError, "") return } + machineRegistrations.WithLabelValues("update", "web", "success", m.Namespace.Name).Inc() c.Data(200, "application/json; charset=utf-8", respBody) return } @@ -141,9 +146,11 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { Str("handler", "Registration"). Err(err). Msg("Cannot encode message") + machineRegistrations.WithLabelValues("new", "web", "error", m.Namespace.Name).Inc() c.String(http.StatusInternalServerError, "") return } + machineRegistrations.WithLabelValues("new", "web", "success", m.Namespace.Name).Inc() c.Data(200, "application/json; charset=utf-8", respBody) return } @@ -213,12 +220,12 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { c.Data(200, "application/json; charset=utf-8", respBody) } -func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m Machine) (*[]byte, error) { +func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m *Machine) ([]byte, error) { log.Trace(). Str("func", "getMapResponse"). Str("machine", req.Hostinfo.Hostname). Msg("Creating Map response") - node, err := h.toNode(m, true) + node, err := m.toNode(true) if err != nil { log.Error(). Str("func", "getMapResponse"). @@ -226,6 +233,7 @@ func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m Mac Msg("Cannot convert to node") return nil, err } + peers, err := h.getPeers(m) if err != nil { log.Error(). @@ -241,6 +249,15 @@ func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m Mac DisplayName: m.Namespace.Name, } + nodePeers, err := peers.toNodes(true) + if err != nil { + log.Error(). + Str("func", "getMapResponse"). + Err(err). + Msg("Failed to convert peers to Tailscale nodes") + return nil, err + } + var dnsConfig *tailcfg.DNSConfig if h.cfg.DNSConfig != nil && h.cfg.DNSConfig.Proxied { // if MagicDNS is enabled // TODO(juanfont): We should not be regenerating this all the time @@ -260,16 +277,19 @@ func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m Mac resp := tailcfg.MapResponse{ KeepAlive: false, Node: node, - Peers: *peers, + Peers: nodePeers, DNSConfig: dnsConfig, Domain: h.cfg.BaseDomain, PacketFilter: *h.aclRules, DERPMap: h.cfg.DerpMap, + + // TODO(juanfont): We should send the profiles of all the peers (this own namespace + those from the shared peers) UserProfiles: []tailcfg.UserProfile{profile}, } log.Trace(). Str("func", "getMapResponse"). Str("machine", req.Hostinfo.Hostname). + Interface("payload", resp). Msgf("Generated map response: %s", tailMapResponseToString(resp)) var respBody []byte @@ -292,10 +312,10 @@ func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m Mac data := make([]byte, 4) binary.LittleEndian.PutUint32(data, uint32(len(respBody))) data = append(data, respBody...) - return &data, nil + return data, nil } -func (h *Headscale) getMapKeepAliveResponse(mKey wgkey.Key, req tailcfg.MapRequest, m Machine) (*[]byte, error) { +func (h *Headscale) getMapKeepAliveResponse(mKey wgkey.Key, req tailcfg.MapRequest, m *Machine) ([]byte, error) { resp := tailcfg.MapResponse{ KeepAlive: true, } @@ -318,7 +338,7 @@ func (h *Headscale) getMapKeepAliveResponse(mKey wgkey.Key, req tailcfg.MapReque data := make([]byte, 4) binary.LittleEndian.PutUint32(data, uint32(len(respBody))) data = append(data, respBody...) - return &data, nil + return data, nil } func (h *Headscale) handleAuthKey(c *gin.Context, db *gorm.DB, idKey wgkey.Key, req tailcfg.RegisterRequest, m Machine) { @@ -343,9 +363,15 @@ func (h *Headscale) handleAuthKey(c *gin.Context, db *gorm.DB, idKey wgkey.Key, Err(err). Msg("Cannot encode message") c.String(http.StatusInternalServerError, "") + machineRegistrations.WithLabelValues("new", "authkey", "error", m.Namespace.Name).Inc() return } - c.Data(200, "application/json; charset=utf-8", respBody) + c.Data(401, "application/json; charset=utf-8", respBody) + log.Error(). + Str("func", "handleAuthKey"). + Str("machine", m.Name). + Msg("Failed authentication via AuthKey") + machineRegistrations.WithLabelValues("new", "authkey", "error", m.Namespace.Name).Inc() return } @@ -359,6 +385,7 @@ func (h *Headscale) handleAuthKey(c *gin.Context, db *gorm.DB, idKey wgkey.Key, Str("func", "handleAuthKey"). Str("machine", m.Name). Msg("Failed to find an available IP") + machineRegistrations.WithLabelValues("new", "authkey", "error", m.Namespace.Name).Inc() return } log.Info(). @@ -384,9 +411,11 @@ func (h *Headscale) handleAuthKey(c *gin.Context, db *gorm.DB, idKey wgkey.Key, Str("machine", m.Name). Err(err). Msg("Cannot encode message") + machineRegistrations.WithLabelValues("new", "authkey", "error", m.Namespace.Name).Inc() c.String(http.StatusInternalServerError, "Extremely sad!") return } + machineRegistrations.WithLabelValues("new", "authkey", "success", m.Namespace.Name).Inc() c.Data(200, "application/json; charset=utf-8", respBody) log.Info(). Str("func", "handleAuthKey"). diff --git a/app.go b/app.go index 44c445e4..31e306dc 100644 --- a/app.go +++ b/app.go @@ -12,6 +12,7 @@ import ( "github.com/rs/zerolog/log" "github.com/gin-gonic/gin" + "github.com/zsais/go-gin-prometheus" "golang.org/x/crypto/acme" "golang.org/x/crypto/acme/autocert" "gorm.io/gorm" @@ -157,6 +158,7 @@ func (h *Headscale) expireEphemeralNodesWorker() { if err != nil { log.Error().Err(err).Str("machine", m.Name).Msg("🤮 Cannot delete ephemeral machine from the database") } + updateRequestsFromNode.WithLabelValues("ephemeral-node-update").Inc() h.notifyChangesToPeers(&m) } } @@ -180,6 +182,10 @@ func (h *Headscale) watchForKVUpdatesWorker() { // Serve launches a GIN server with the Headscale API func (h *Headscale) Serve() error { r := gin.Default() + + p := ginprometheus.NewPrometheus("gin") + p.Use(r) + r.GET("/health", func(c *gin.Context) { c.JSON(200, gin.H{"healthy": "ok"}) }) r.GET("/key", h.KeyHandler) r.GET("/register", h.RegisterWebAPI) @@ -254,6 +260,7 @@ func (h *Headscale) Serve() error { func (h *Headscale) setLastStateChangeToNow(namespace string) { now := time.Now().UTC() + lastStateUpdate.WithLabelValues("", "headscale").Set(float64(now.Unix())) h.lastStateChange.Store(namespace, now) } diff --git a/go.mod b/go.mod index 3a49c0fd..70428315 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect github.com/opencontainers/runc v1.0.2 // indirect github.com/ory/dockertest/v3 v3.7.0 + github.com/prometheus/client_golang v1.11.0 // indirect github.com/pterm/pterm v0.12.30 github.com/rs/zerolog v1.25.0 github.com/spf13/cobra v1.2.1 @@ -28,6 +29,7 @@ require ( github.com/tailscale/hujson v0.0.0-20210818175511-7360507a6e88 github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/zsais/go-gin-prometheus v0.1.0 // indirect golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 golang.org/x/net v0.0.0-20210913180222-943fd674d43e // indirect golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0 // indirect diff --git a/go.sum b/go.sum index 1d97f18f..6dc92b6d 100644 --- a/go.sum +++ b/go.sum @@ -103,6 +103,7 @@ github.com/aws/aws-sdk-go v1.38.52/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2z github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= @@ -118,7 +119,9 @@ github.com/cenkalti/backoff/v4 v4.1.0/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInq github.com/cenkalti/backoff/v4 v4.1.1 h1:G2HAfAmvm/GcKan2oOQpBXOd2tT2G57ZnZGWa1PxPBQ= github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -518,6 +521,7 @@ github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhB github.com/jmoiron/sqlx v1.2.1-0.20190826204134-d7d95172beb5/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ= github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok= @@ -531,6 +535,7 @@ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= @@ -538,6 +543,7 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= @@ -616,6 +622,7 @@ github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGw github.com/mattn/go-sqlite3 v1.14.8 h1:gDp86IdQsN/xWjIEmr9MF6o9mpksUgh0fu+9ByFxzIU= github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mbilski/exhaustivestruct v1.1.0/go.mod h1:OeTBVxQWoEmB2J2JCHmXWPJ0aksxSUOUy+nvtVEfzXc= github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y= @@ -669,6 +676,7 @@ github.com/moricho/tparallel v0.2.1/go.mod h1:fXEIZxG2vdfl0ZF8b42f5a78EhjjD5mX8q github.com/mozilla/tls-observatory v0.0.0-20200317151703-4fa42e1c2dee/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk= github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nakabonne/nestif v0.3.0/go.mod h1:dI314BppzXjJ4HsCnbo7XzrJHPszZsjnk5wEBSYHI2c= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= @@ -747,21 +755,32 @@ github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= @@ -929,6 +948,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/ziutek/telnet v0.0.0-20180329124119-c3b780dc415b/go.mod h1:IZpXDfkJ6tWD3PhBK5YzgQT+xJWh7OsdwiG8hA2MkO4= +github.com/zsais/go-gin-prometheus v0.1.0 h1:bkLv1XCdzqVgQ36ScgRi09MA2UC1t3tAB6nsfErsGO4= +github.com/zsais/go-gin-prometheus v0.1.0/go.mod h1:Slirjzuz8uM8Cw0jmPNqbneoqcUtY2GGjn2bEd4NRLY= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= @@ -1143,6 +1164,7 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1158,6 +1180,8 @@ golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1190,6 +1214,7 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1450,6 +1475,7 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/integration_test.go b/integration_test.go index 01441591..9bc51f99 100644 --- a/integration_test.go +++ b/integration_test.go @@ -7,6 +7,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io/ioutil" "log" @@ -233,9 +234,6 @@ func (s *IntegrationTestSuite) SetupSuite() { }, Networks: []*dockertest.Network{&network}, Cmd: []string{"headscale", "serve"}, - PortBindings: map[docker.Port][]docker.PortBinding{ - "8080/tcp": {{HostPort: "8080"}}, - }, } fmt.Println("Creating headscale container") @@ -270,7 +268,11 @@ func (s *IntegrationTestSuite) SetupSuite() { } return nil }); err != nil { - log.Fatalf("Could not connect to docker: %s", err) + // TODO(kradalby): If we cannot access headscale, or any other fatal error during + // test setup, we need to abort and tear down. However, testify does not seem to + // support that at the moment: + // https://github.com/stretchr/testify/issues/849 + return // fmt.Errorf("Could not connect to headscale: %s", err) } fmt.Println("headscale container is ready") @@ -292,7 +294,7 @@ func (s *IntegrationTestSuite) SetupSuite() { ) assert.Nil(s.T(), err) - headscaleEndpoint := fmt.Sprintf("http://headscale:%s", headscale.GetPort("8080/tcp")) + headscaleEndpoint := "http://headscale:8080" fmt.Printf("Joining tailscale containers to headscale at %s\n", headscaleEndpoint) for hostname, tailscale := range scales.tailscales { @@ -353,15 +355,16 @@ func (s *IntegrationTestSuite) TestGetIpAddresses() { for hostname := range scales.tailscales { s.T().Run(hostname, func(t *testing.T) { - ip := ips[hostname] + ip, ok := ips[hostname] + + assert.True(t, ok) + assert.NotNil(t, ip) fmt.Printf("IP for %s: %s\n", hostname, ip) // c.Assert(ip.Valid(), check.IsTrue) assert.True(t, ip.Is4()) assert.True(t, ipPrefix.Contains(ip)) - - ips[hostname] = ip }) } } @@ -724,6 +727,9 @@ func getAPIURLs(tailscales map[string]dockertest.Resource) (map[netaddr.IP]strin n := ft.Node for _, a := range n.Addresses { // just add all the addresses if _, ok := fts[a.IP()]; !ok { + if ft.PeerAPIURL == "" { + return nil, errors.New("api url is empty") + } fts[a.IP()] = ft.PeerAPIURL } } diff --git a/integration_test/etc/config.json b/integration_test/etc/config.json index 8868d1be..dc23652d 100644 --- a/integration_test/etc/config.json +++ b/integration_test/etc/config.json @@ -7,7 +7,7 @@ "db_type": "sqlite3", "db_path": "/tmp/integration_test_db.sqlite3", "acl_policy_path": "", - "log_level": "debug", + "log_level": "trace", "dns_config": { "nameservers": [ "1.1.1.1" diff --git a/machine.go b/machine.go index 5b045e3b..7a9df2e8 100644 --- a/machine.go +++ b/machine.go @@ -6,6 +6,7 @@ import ( "fmt" "sort" "strconv" + "strings" "time" "github.com/rs/zerolog/log" @@ -45,14 +46,338 @@ type Machine struct { DeletedAt *time.Time } +type ( + Machines []Machine + MachinesP []*Machine +) + // For the time being this method is rather naive func (m Machine) isAlreadyRegistered() bool { return m.Registered } +func (h *Headscale) getDirectPeers(m *Machine) (Machines, error) { + log.Trace(). + Str("func", "getDirectPeers"). + Str("machine", m.Name). + Msg("Finding direct peers") + + machines := Machines{} + if err := h.db.Where("namespace_id = ? AND machine_key <> ? AND registered", + m.NamespaceID, m.MachineKey).Find(&machines).Error; err != nil { + log.Error().Err(err).Msg("Error accessing db") + return nil, err + } + + sort.Slice(machines, func(i, j int) bool { return machines[i].ID < machines[j].ID }) + + log.Trace(). + Str("func", "getDirectmachines"). + Str("machine", m.Name). + Msgf("Found direct machines: %s", machines.String()) + return machines, nil +} + +func (h *Headscale) getShared(m *Machine) (Machines, error) { + log.Trace(). + Str("func", "getShared"). + Str("machine", m.Name). + Msg("Finding shared peers") + + // We fetch here machines that are shared to the `Namespace` of the machine we are getting peers for + sharedMachines := []SharedMachine{} + if err := h.db.Preload("Namespace").Preload("Machine").Where("namespace_id = ?", + m.NamespaceID).Find(&sharedMachines).Error; err != nil { + return nil, err + } + + peers := make(Machines, 0) + for _, sharedMachine := range sharedMachines { + peers = append(peers, sharedMachine.Machine) + } + + sort.Slice(peers, func(i, j int) bool { return peers[i].ID < peers[j].ID }) + + log.Trace(). + Str("func", "getShared"). + Str("machine", m.Name). + Msgf("Found shared peers: %s", peers.String()) + return peers, nil +} + +func (h *Headscale) getPeers(m *Machine) (Machines, error) { + direct, err := h.getDirectPeers(m) + if err != nil { + log.Error(). + Str("func", "getPeers"). + Err(err). + Msg("Cannot fetch peers") + return nil, err + } + + shared, err := h.getShared(m) + if err != nil { + log.Error(). + Str("func", "getDirectPeers"). + Err(err). + Msg("Cannot fetch peers") + return nil, err + } + + peers := append(direct, shared...) + sort.Slice(peers, func(i, j int) bool { return peers[i].ID < peers[j].ID }) + + log.Trace(). + Str("func", "getShared"). + Str("machine", m.Name). + Msgf("Found total peers: %s", peers.String()) + + return peers, nil +} + +// GetMachine finds a Machine by name and namespace and returns the Machine struct +func (h *Headscale) GetMachine(namespace string, name string) (*Machine, error) { + machines, err := h.ListMachinesInNamespace(namespace) + if err != nil { + return nil, err + } + + for _, m := range *machines { + if m.Name == name { + return &m, nil + } + } + return nil, fmt.Errorf("machine not found") +} + +// GetMachineByID finds a Machine by ID and returns the Machine struct +func (h *Headscale) GetMachineByID(id uint64) (*Machine, error) { + m := Machine{} + if result := h.db.Preload("Namespace").Find(&Machine{ID: id}).First(&m); result.Error != nil { + return nil, result.Error + } + return &m, nil +} + +// GetMachineByMachineKey finds a Machine by ID and returns the Machine struct +func (h *Headscale) GetMachineByMachineKey(mKey string) (*Machine, error) { + m := Machine{} + if result := h.db.Preload("Namespace").First(&m, "machine_key = ?", mKey); result.Error != nil { + return nil, result.Error + } + return &m, nil +} + +// UpdateMachine takes a Machine struct pointer (typically already loaded from database +// and updates it with the latest data from the database. +func (h *Headscale) UpdateMachine(m *Machine) error { + if result := h.db.Find(m).First(&m); result.Error != nil { + return result.Error + } + return nil +} + +// DeleteMachine softs deletes a Machine from the database +func (h *Headscale) DeleteMachine(m *Machine) error { + m.Registered = false + namespaceID := m.NamespaceID + h.db.Save(&m) // we mark it as unregistered, just in case + if err := h.db.Delete(&m).Error; err != nil { + return err + } + + return h.RequestMapUpdates(namespaceID) +} + +// HardDeleteMachine hard deletes a Machine from the database +func (h *Headscale) HardDeleteMachine(m *Machine) error { + namespaceID := m.NamespaceID + if err := h.db.Unscoped().Delete(&m).Error; err != nil { + return err + } + return h.RequestMapUpdates(namespaceID) +} + +// GetHostInfo returns a Hostinfo struct for the machine +func (m *Machine) GetHostInfo() (*tailcfg.Hostinfo, error) { + hostinfo := tailcfg.Hostinfo{} + if len(m.HostInfo) != 0 { + hi, err := m.HostInfo.MarshalJSON() + if err != nil { + return nil, err + } + err = json.Unmarshal(hi, &hostinfo) + if err != nil { + return nil, err + } + } + return &hostinfo, nil +} + +func (h *Headscale) notifyChangesToPeers(m *Machine) { + peers, err := h.getPeers(m) + if err != nil { + log.Error(). + Str("func", "notifyChangesToPeers"). + Str("machine", m.Name). + Msgf("Error getting peers: %s", err) + return + } + for _, peer := range peers { + log.Info(). + Str("func", "notifyChangesToPeers"). + Str("machine", m.Name). + Str("peer", peer.Name). + Str("address", peer.IPAddress). + Msgf("Notifying peer %s (%s)", peer.Name, peer.IPAddress) + err := h.sendRequestOnUpdateChannel(&peer) + if err != nil { + log.Info(). + Str("func", "notifyChangesToPeers"). + Str("machine", m.Name). + Str("peer", peer.Name). + Msgf("Peer %s does not have an open update client, skipping.", peer.Name) + continue + } + log.Trace(). + Str("func", "notifyChangesToPeers"). + Str("machine", m.Name). + Str("peer", peer.Name). + Str("address", peer.IPAddress). + Msgf("Notified peer %s (%s)", peer.Name, peer.IPAddress) + } +} + +func (h *Headscale) getOrOpenUpdateChannel(m *Machine) <-chan struct{} { + var updateChan chan struct{} + if storedChan, ok := h.clientsUpdateChannels.Load(m.ID); ok { + if unwrapped, ok := storedChan.(chan struct{}); ok { + updateChan = unwrapped + } else { + log.Error(). + Str("handler", "openUpdateChannel"). + Str("machine", m.Name). + Msg("Failed to convert update channel to struct{}") + } + } else { + log.Debug(). + Str("handler", "openUpdateChannel"). + Str("machine", m.Name). + Msg("Update channel not found, creating") + + updateChan = make(chan struct{}) + h.clientsUpdateChannels.Store(m.ID, updateChan) + } + return updateChan +} + +func (h *Headscale) closeUpdateChannel(m *Machine) { + h.clientsUpdateChannelMutex.Lock() + defer h.clientsUpdateChannelMutex.Unlock() + + if storedChan, ok := h.clientsUpdateChannels.Load(m.ID); ok { + if unwrapped, ok := storedChan.(chan struct{}); ok { + close(unwrapped) + } + } + h.clientsUpdateChannels.Delete(m.ID) +} + +func (h *Headscale) sendRequestOnUpdateChannel(m *Machine) error { + h.clientsUpdateChannelMutex.Lock() + defer h.clientsUpdateChannelMutex.Unlock() + + pUp, ok := h.clientsUpdateChannels.Load(uint64(m.ID)) + if ok { + log.Info(). + Str("func", "requestUpdate"). + Str("machine", m.Name). + Msgf("Notifying peer %s", m.Name) + + if update, ok := pUp.(chan struct{}); ok { + log.Trace(). + Str("func", "requestUpdate"). + Str("machine", m.Name). + Msgf("Update channel is %#v", update) + + updateRequestsToNode.Inc() + update <- struct{}{} + + log.Trace(). + Str("func", "requestUpdate"). + Str("machine", m.Name). + Msgf("Notified machine %s", m.Name) + } + } else { + err := errors.New("machine does not have an open update channel") + log.Info(). + Str("func", "requestUpdate"). + Str("machine", m.Name). + Msgf("Machine %s does not have an open update channel", m.Name) + return err + } + return nil +} + +func (h *Headscale) isOutdated(m *Machine) bool { + err := h.UpdateMachine(m) + if err != nil { + return true + } + + lastChange := h.getLastStateChange(m.Namespace.Name) + log.Trace(). + Str("func", "keepAlive"). + Str("machine", m.Name). + Time("last_successful_update", *m.LastSuccessfulUpdate). + Time("last_state_change", lastChange). + Msgf("Checking if %s is missing updates", m.Name) + return m.LastSuccessfulUpdate.Before(lastChange) +} + +func (m Machine) String() string { + return m.Name +} + +func (ms Machines) String() string { + temp := make([]string, len(ms)) + + for index, machine := range ms { + temp[index] = machine.Name + } + + return fmt.Sprintf("[ %s ](%d)", strings.Join(temp, ", "), len(temp)) +} + +// TODO(kradalby): Remove when we have generics... +func (ms MachinesP) String() string { + temp := make([]string, len(ms)) + + for index, machine := range ms { + temp[index] = machine.Name + } + + return fmt.Sprintf("[ %s ](%d)", strings.Join(temp, ", "), len(temp)) +} + +func (ms Machines) toNodes(includeRoutes bool) ([]*tailcfg.Node, error) { + nodes := make([]*tailcfg.Node, len(ms)) + + for index, machine := range ms { + node, err := machine.toNode(includeRoutes) + if err != nil { + return nil, err + } + + nodes[index] = node + } + + return nodes, nil +} + // toNode converts a Machine into a Tailscale Node. includeRoutes is false for shared nodes // as per the expected behaviour in the official SaaS -func (m *Machine) toNode(baseDomain string, dnsConfig *tailcfg.DNSConfig, includeRoutes bool) (*tailcfg.Node, error) { +func (m Machine) toNode(includeRoutes bool) (*tailcfg.Node, error) { nKey, err := wgkey.ParseHex(m.NodeKey) if err != nil { return nil, err @@ -147,17 +472,10 @@ func (m *Machine) toNode(baseDomain string, dnsConfig *tailcfg.DNSConfig, includ keyExpiry = time.Time{} } - var hostname string - if dnsConfig != nil && dnsConfig.Proxied { // MagicDNS - hostname = fmt.Sprintf("%s.%s.%s", m.Name, m.Namespace.Name, baseDomain) - } else { - hostname = m.Name - } - n := tailcfg.Node{ ID: tailcfg.NodeID(m.ID), // this is the actual ID StableID: tailcfg.StableNodeID(strconv.FormatUint(m.ID, 10)), // in headscale, unlike tailcontrol server, IDs are permanent - Name: hostname, + Name: hostinfo.Hostname, User: tailcfg.UserID(m.NamespaceID), Key: tailcfg.NodeKey(nKey), KeyExpiry: keyExpiry, @@ -176,241 +494,5 @@ func (m *Machine) toNode(baseDomain string, dnsConfig *tailcfg.DNSConfig, includ MachineAuthorized: m.Registered, Capabilities: []string{tailcfg.CapabilityFileSharing}, } - // TODO(juanfont): Node also has Sharer when is a shared node with info on the profile - return &n, nil } - -func (h *Headscale) getPeers(m Machine) (*[]*tailcfg.Node, error) { - log.Trace(). - Str("func", "getPeers"). - Str("machine", m.Name). - Msg("Finding peers") - - machines := []Machine{} - if err := h.db.Preload("Namespace").Where("namespace_id = ? AND machine_key <> ? AND registered", - m.NamespaceID, m.MachineKey).Find(&machines).Error; err != nil { - log.Error().Err(err).Msg("Error accessing db") - return nil, err - } - - // We fetch here machines that are shared to the `Namespace` of the machine we are getting peers for - sharedMachines := []SharedMachine{} - if err := h.db.Preload("Namespace").Preload("Machine").Where("namespace_id = ?", - m.NamespaceID).Find(&sharedMachines).Error; err != nil { - return nil, err - } - - peers := []*tailcfg.Node{} - for _, mn := range machines { - peer, err := mn.toNode(h.cfg.BaseDomain, h.cfg.DNSConfig, true) - if err != nil { - return nil, err - } - peers = append(peers, peer) - } - for _, sharedMachine := range sharedMachines { - peer, err := sharedMachine.Machine.toNode(h.cfg.BaseDomain, h.cfg.DNSConfig, false) // shared nodes do not expose their routes - if err != nil { - return nil, err - } - peers = append(peers, peer) - } - sort.Slice(peers, func(i, j int) bool { return peers[i].ID < peers[j].ID }) - - log.Trace(). - Str("func", "getPeers"). - Str("machine", m.Name). - Msgf("Found peers: %s", tailNodesToString(peers)) - return &peers, nil -} - -// GetMachine finds a Machine by name and namespace and returns the Machine struct -func (h *Headscale) GetMachine(namespace string, name string) (*Machine, error) { - machines, err := h.ListMachinesInNamespace(namespace) - if err != nil { - return nil, err - } - - for _, m := range *machines { - if m.Name == name { - return &m, nil - } - } - return nil, fmt.Errorf("machine not found") -} - -// GetMachineByID finds a Machine by ID and returns the Machine struct -func (h *Headscale) GetMachineByID(id uint64) (*Machine, error) { - m := Machine{} - if result := h.db.Preload("Namespace").Find(&Machine{ID: id}).First(&m); result.Error != nil { - return nil, result.Error - } - return &m, nil -} - -// UpdateMachine takes a Machine struct pointer (typically already loaded from database -// and updates it with the latest data from the database. -func (h *Headscale) UpdateMachine(m *Machine) error { - if result := h.db.Find(m).First(&m); result.Error != nil { - return result.Error - } - return nil -} - -// DeleteMachine softs deletes a Machine from the database -func (h *Headscale) DeleteMachine(m *Machine) error { - m.Registered = false - namespaceID := m.NamespaceID - h.db.Save(&m) // we mark it as unregistered, just in case - if err := h.db.Delete(&m).Error; err != nil { - return err - } - - return h.RequestMapUpdates(namespaceID) -} - -// HardDeleteMachine hard deletes a Machine from the database -func (h *Headscale) HardDeleteMachine(m *Machine) error { - namespaceID := m.NamespaceID - if err := h.db.Unscoped().Delete(&m).Error; err != nil { - return err - } - return h.RequestMapUpdates(namespaceID) -} - -// GetHostInfo returns a Hostinfo struct for the machine -func (m *Machine) GetHostInfo() (*tailcfg.Hostinfo, error) { - hostinfo := tailcfg.Hostinfo{} - if len(m.HostInfo) != 0 { - hi, err := m.HostInfo.MarshalJSON() - if err != nil { - return nil, err - } - err = json.Unmarshal(hi, &hostinfo) - if err != nil { - return nil, err - } - } - return &hostinfo, nil -} - -func (h *Headscale) notifyChangesToPeers(m *Machine) { - peers, err := h.getPeers(*m) - if err != nil { - log.Error(). - Str("func", "notifyChangesToPeers"). - Str("machine", m.Name). - Msgf("Error getting peers: %s", err) - return - } - for _, p := range *peers { - log.Info(). - Str("func", "notifyChangesToPeers"). - Str("machine", m.Name). - Str("peer", p.Name). - Str("address", p.Addresses[0].String()). - Msgf("Notifying peer %s (%s)", p.Name, p.Addresses[0]) - err := h.sendRequestOnUpdateChannel(p) - if err != nil { - log.Info(). - Str("func", "notifyChangesToPeers"). - Str("machine", m.Name). - Str("peer", p.Name). - Msgf("Peer %s does not have an open update client, skipping.", p.Name) - continue - } - log.Trace(). - Str("func", "notifyChangesToPeers"). - Str("machine", m.Name). - Str("peer", p.Name). - Str("address", p.Addresses[0].String()). - Msgf("Notified peer %s (%s)", p.Name, p.Addresses[0]) - } -} - -func (h *Headscale) getOrOpenUpdateChannel(m *Machine) <-chan struct{} { - var updateChan chan struct{} - if storedChan, ok := h.clientsUpdateChannels.Load(m.ID); ok { - if unwrapped, ok := storedChan.(chan struct{}); ok { - updateChan = unwrapped - } else { - log.Error(). - Str("handler", "openUpdateChannel"). - Str("machine", m.Name). - Msg("Failed to convert update channel to struct{}") - } - } else { - log.Debug(). - Str("handler", "openUpdateChannel"). - Str("machine", m.Name). - Msg("Update channel not found, creating") - - updateChan = make(chan struct{}) - h.clientsUpdateChannels.Store(m.ID, updateChan) - } - return updateChan -} - -func (h *Headscale) closeUpdateChannel(m *Machine) { - h.clientsUpdateChannelMutex.Lock() - defer h.clientsUpdateChannelMutex.Unlock() - - if storedChan, ok := h.clientsUpdateChannels.Load(m.ID); ok { - if unwrapped, ok := storedChan.(chan struct{}); ok { - close(unwrapped) - } - } - h.clientsUpdateChannels.Delete(m.ID) -} - -func (h *Headscale) sendRequestOnUpdateChannel(m *tailcfg.Node) error { - h.clientsUpdateChannelMutex.Lock() - defer h.clientsUpdateChannelMutex.Unlock() - - pUp, ok := h.clientsUpdateChannels.Load(uint64(m.ID)) - if ok { - log.Info(). - Str("func", "requestUpdate"). - Str("machine", m.Name). - Msgf("Notifying peer %s", m.Name) - - if update, ok := pUp.(chan struct{}); ok { - log.Trace(). - Str("func", "requestUpdate"). - Str("machine", m.Name). - Msgf("Update channel is %#v", update) - - update <- struct{}{} - - log.Trace(). - Str("func", "requestUpdate"). - Str("machine", m.Name). - Msgf("Notified machine %s", m.Name) - } - } else { - err := errors.New("machine does not have an open update channel") - log.Info(). - Str("func", "requestUpdate"). - Str("machine", m.Name). - Msgf("Machine %s does not have an open update channel", m.Name) - return err - } - return nil -} - -func (h *Headscale) isOutdated(m *Machine) bool { - err := h.UpdateMachine(m) - if err != nil { - return true - } - - lastChange := h.getLastStateChange(m.Namespace.Name) - log.Trace(). - Str("func", "keepAlive"). - Str("machine", m.Name). - Time("last_successful_update", *m.LastSuccessfulUpdate). - Time("last_state_change", lastChange). - Msgf("Checking if %s is missing updates", m.Name) - return m.LastSuccessfulUpdate.Before(lastChange) -} diff --git a/machine_test.go b/machine_test.go index d535be56..dfe84d33 100644 --- a/machine_test.go +++ b/machine_test.go @@ -2,6 +2,7 @@ package headscale import ( "encoding/json" + "strconv" "gopkg.in/check.v1" ) @@ -16,7 +17,7 @@ func (s *Suite) TestGetMachine(c *check.C) { _, err = h.GetMachine("test", "testmachine") c.Assert(err, check.NotNil) - m := Machine{ + m := &Machine{ ID: 0, MachineKey: "foo", NodeKey: "bar", @@ -27,7 +28,7 @@ func (s *Suite) TestGetMachine(c *check.C) { RegisterMethod: "authKey", AuthKeyID: uint(pak.ID), } - h.db.Save(&m) + h.db.Save(m) m1, err := h.GetMachine("test", "testmachine") c.Assert(err, check.IsNil) @@ -116,3 +117,43 @@ func (s *Suite) TestHardDeleteMachine(c *check.C) { _, err = h.GetMachine(n.Name, "testmachine3") c.Assert(err, check.NotNil) } + +func (s *Suite) TestGetDirectPeers(c *check.C) { + n, err := h.CreateNamespace("test") + c.Assert(err, check.IsNil) + + pak, err := h.CreatePreAuthKey(n.Name, false, false, nil) + c.Assert(err, check.IsNil) + + _, err = h.GetMachineByID(0) + c.Assert(err, check.NotNil) + + for i := 0; i <= 10; i++ { + m := Machine{ + ID: uint64(i), + MachineKey: "foo" + strconv.Itoa(i), + NodeKey: "bar" + strconv.Itoa(i), + DiscoKey: "faa" + strconv.Itoa(i), + Name: "testmachine" + strconv.Itoa(i), + NamespaceID: n.ID, + Registered: true, + RegisterMethod: "authKey", + AuthKeyID: uint(pak.ID), + } + h.db.Save(&m) + } + + m1, err := h.GetMachineByID(0) + c.Assert(err, check.IsNil) + + _, err = m1.GetHostInfo() + c.Assert(err, check.IsNil) + + peers, err := h.getDirectPeers(m1) + c.Assert(err, check.IsNil) + + c.Assert(len(peers), check.Equals, 9) + c.Assert(peers[0].Name, check.Equals, "testmachine2") + c.Assert(peers[5].Name, check.Equals, "testmachine7") + c.Assert(peers[8].Name, check.Equals, "testmachine10") +} diff --git a/metrics.go b/metrics.go new file mode 100644 index 00000000..d516fade --- /dev/null +++ b/metrics.go @@ -0,0 +1,41 @@ +package headscale + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +const prometheusNamespace = "headscale" + +var ( + // This is a high cardinality metric (namespace x machines), we might want to make this + // configurable/opt-in in the future. + lastStateUpdate = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: prometheusNamespace, + Name: "last_update_seconds", + Help: "Time stamp in unix time when a machine or headscale was updated", + }, []string{"namespace", "machine"}) + + machineRegistrations = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: prometheusNamespace, + Name: "machine_registrations_total", + Help: "The total amount of registered machine attempts", + }, []string{"action", "auth", "status", "namespace"}) + + updateRequestsFromNode = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: prometheusNamespace, + Name: "update_request_from_node_total", + Help: "The number of updates requested by a node/update function", + }, []string{"state"}) + updateRequestsToNode = promauto.NewCounter(prometheus.CounterOpts{ + Namespace: prometheusNamespace, + Name: "update_request_to_node_total", + Help: "The number of calls/messages issued on a specific nodes update channel", + }) + //TODO(kradalby): This is very debugging, we might want to remove it. + updateRequestsReceivedOnChannel = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: prometheusNamespace, + Name: "update_request_received_on_channel_total", + Help: "The number of update requests received on an update channel", + }, []string{"machine"}) +) diff --git a/namespaces.go b/namespaces.go index 8204f96d..75b6eab5 100644 --- a/namespaces.go +++ b/namespaces.go @@ -191,6 +191,7 @@ func (h *Headscale) checkForNamespacesPendingUpdates() { continue } for _, m := range *machines { + updateRequestsFromNode.WithLabelValues("namespace-update").Inc() h.notifyChangesToPeers(&m) } } diff --git a/poll.go b/poll.go index f128fab1..a33d341b 100644 --- a/poll.go +++ b/poll.go @@ -51,13 +51,19 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) { return } - var m Machine - if result := h.db.Preload("Namespace").First(&m, "machine_key = ?", mKey.HexString()); errors.Is(result.Error, gorm.ErrRecordNotFound) { - log.Warn(). + m, err := h.GetMachineByMachineKey(mKey.HexString()) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + log.Warn(). + Str("handler", "PollNetMap"). + Msgf("Ignoring request, cannot find machine with key %s", mKey.HexString()) + c.String(http.StatusUnauthorized, "") + return + } + log.Error(). Str("handler", "PollNetMap"). - Msgf("Ignoring request, cannot find machine with key %s", mKey.HexString()) - c.String(http.StatusUnauthorized, "") - return + Msgf("Failed to fetch machine from the database with Machine key: %s", mKey.HexString()) + c.String(http.StatusInternalServerError, "") } log.Trace(). Str("handler", "PollNetMap"). @@ -117,7 +123,7 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) { Str("handler", "PollNetMap"). Str("machine", m.Name). Msg("Client is starting up. Probably interested in a DERP map") - c.Data(200, "application/json; charset=utf-8", *data) + c.Data(200, "application/json; charset=utf-8", data) return } @@ -134,7 +140,7 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) { Str("id", c.Param("id")). Str("machine", m.Name). Msg("Loading or creating update channel") - updateChan := h.getOrOpenUpdateChannel(&m) + updateChan := h.getOrOpenUpdateChannel(m) pollDataChan := make(chan []byte) // defer close(pollData) @@ -149,11 +155,12 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) { Str("handler", "PollNetMap"). Str("machine", m.Name). Msg("Client sent endpoint update and is ok with a response without peer list") - c.Data(200, "application/json; charset=utf-8", *data) + c.Data(200, "application/json; charset=utf-8", data) // It sounds like we should update the nodes when we have received a endpoint update // even tho the comments in the tailscale code dont explicitly say so. - go h.notifyChangesToPeers(&m) + updateRequestsFromNode.WithLabelValues("endpoint-update").Inc() + go h.notifyChangesToPeers(m) return } else if req.OmitPeers && req.Stream { log.Warn(). @@ -172,13 +179,14 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) { Str("handler", "PollNetMap"). Str("machine", m.Name). Msg("Sending initial map") - go func() { pollDataChan <- *data }() + go func() { pollDataChan <- data }() log.Info(). Str("handler", "PollNetMap"). Str("machine", m.Name). Msg("Notifying peers") - go h.notifyChangesToPeers(&m) + updateRequestsFromNode.WithLabelValues("full-update").Inc() + go h.notifyChangesToPeers(m) h.PollNetMapStream(c, m, req, mKey, pollDataChan, keepAliveChan, updateChan, cancelKeepAlive) log.Trace(). @@ -193,7 +201,7 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) { // to the connected clients. func (h *Headscale) PollNetMapStream( c *gin.Context, - m Machine, + m *Machine, req tailcfg.MapRequest, mKey wgkey.Key, pollDataChan chan []byte, @@ -241,7 +249,7 @@ func (h *Headscale) PollNetMapStream( // TODO(kradalby): Abstract away all the database calls, this can cause race conditions // when an outdated machine object is kept alive, e.g. db is update from // command line, but then overwritten. - err = h.UpdateMachine(&m) + err = h.UpdateMachine(m) if err != nil { log.Error(). Str("handler", "PollNetMapStream"). @@ -252,7 +260,10 @@ func (h *Headscale) PollNetMapStream( } now := time.Now().UTC() m.LastSeen = &now + + lastStateUpdate.WithLabelValues(m.Namespace.Name, m.Name).Set(float64(now.Unix())) m.LastSuccessfulUpdate = &now + h.db.Save(&m) log.Trace(). Str("handler", "PollNetMapStream"). @@ -288,7 +299,7 @@ func (h *Headscale) PollNetMapStream( // TODO(kradalby): Abstract away all the database calls, this can cause race conditions // when an outdated machine object is kept alive, e.g. db is update from // command line, but then overwritten. - err = h.UpdateMachine(&m) + err = h.UpdateMachine(m) if err != nil { log.Error(). Str("handler", "PollNetMapStream"). @@ -314,7 +325,8 @@ func (h *Headscale) PollNetMapStream( Str("machine", m.Name). Str("channel", "update"). Msg("Received a request for update") - if h.isOutdated(&m) { + updateRequestsReceivedOnChannel.WithLabelValues(m.Name).Inc() + if h.isOutdated(m) { log.Debug(). Str("handler", "PollNetMapStream"). Str("machine", m.Name). @@ -330,7 +342,7 @@ func (h *Headscale) PollNetMapStream( Err(err). Msg("Could not get the map update") } - _, err = w.Write(*data) + _, err = w.Write(data) if err != nil { log.Error(). Str("handler", "PollNetMapStream"). @@ -353,7 +365,7 @@ func (h *Headscale) PollNetMapStream( // TODO(kradalby): Abstract away all the database calls, this can cause race conditions // when an outdated machine object is kept alive, e.g. db is update from // command line, but then overwritten. - err = h.UpdateMachine(&m) + err = h.UpdateMachine(m) if err != nil { log.Error(). Str("handler", "PollNetMapStream"). @@ -363,7 +375,10 @@ func (h *Headscale) PollNetMapStream( Msg("Cannot update machine from database") } now := time.Now().UTC() + + lastStateUpdate.WithLabelValues(m.Namespace.Name, m.Name).Set(float64(now.Unix())) m.LastSuccessfulUpdate = &now + h.db.Save(&m) } else { log.Trace(). @@ -383,7 +398,7 @@ func (h *Headscale) PollNetMapStream( // TODO: Abstract away all the database calls, this can cause race conditions // when an outdated machine object is kept alive, e.g. db is update from // command line, but then overwritten. - err := h.UpdateMachine(&m) + err := h.UpdateMachine(m) if err != nil { log.Error(). Str("handler", "PollNetMapStream"). @@ -408,21 +423,21 @@ func (h *Headscale) PollNetMapStream( Str("machine", m.Name). Str("channel", "Done"). Msg("Closing update channel") - h.closeUpdateChannel(&m) + h.closeUpdateChannel(m) - close(pollDataChan) log.Trace(). Str("handler", "PollNetMapStream"). Str("machine", m.Name). Str("channel", "Done"). Msg("Closing pollData channel") + close(pollDataChan) - close(keepAliveChan) log.Trace(). Str("handler", "PollNetMapStream"). Str("machine", m.Name). Str("channel", "Done"). Msg("Closing keepAliveChan channel") + close(keepAliveChan) return false } @@ -434,7 +449,7 @@ func (h *Headscale) scheduledPollWorker( keepAliveChan chan<- []byte, mKey wgkey.Key, req tailcfg.MapRequest, - m Machine, + m *Machine, ) { keepAliveTicker := time.NewTicker(60 * time.Second) updateCheckerTicker := time.NewTicker(30 * time.Second) @@ -458,13 +473,12 @@ func (h *Headscale) scheduledPollWorker( Str("func", "keepAlive"). Str("machine", m.Name). Msg("Sending keepalive") - keepAliveChan <- *data + keepAliveChan <- data case <-updateCheckerTicker.C: // Send an update request regardless of outdated or not, if data is sent // to the node is determined in the updateChan consumer block - n, _ := h.toNode(m, true) - err := h.sendRequestOnUpdateChannel(n) + err := h.sendRequestOnUpdateChannel(m) if err != nil { log.Error(). Str("func", "keepAlive"). diff --git a/sharing_test.go b/sharing_test.go index ec4951de..baa90d0c 100644 --- a/sharing_test.go +++ b/sharing_test.go @@ -2,7 +2,6 @@ package headscale import ( "gopkg.in/check.v1" - "tailscale.com/tailcfg" ) func (s *Suite) TestBasicSharedNodesInNamespace(c *check.C) { @@ -21,7 +20,7 @@ func (s *Suite) TestBasicSharedNodesInNamespace(c *check.C) { _, err = h.GetMachine(n1.Name, "test_get_shared_nodes_1") c.Assert(err, check.NotNil) - m1 := Machine{ + m1 := &Machine{ ID: 0, MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", @@ -33,12 +32,12 @@ func (s *Suite) TestBasicSharedNodesInNamespace(c *check.C) { IPAddress: "100.64.0.1", AuthKeyID: uint(pak1.ID), } - h.db.Save(&m1) + h.db.Save(m1) _, err = h.GetMachine(n1.Name, m1.Name) c.Assert(err, check.IsNil) - m2 := Machine{ + m2 := &Machine{ ID: 1, MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", @@ -50,22 +49,22 @@ func (s *Suite) TestBasicSharedNodesInNamespace(c *check.C) { IPAddress: "100.64.0.2", AuthKeyID: uint(pak2.ID), } - h.db.Save(&m2) + h.db.Save(m2) _, err = h.GetMachine(n2.Name, m2.Name) c.Assert(err, check.IsNil) p1s, err := h.getPeers(m1) c.Assert(err, check.IsNil) - c.Assert(len(*p1s), check.Equals, 0) + c.Assert(len(p1s), check.Equals, 0) - err = h.AddSharedMachineToNamespace(&m2, n1) + err = h.AddSharedMachineToNamespace(m2, n1) c.Assert(err, check.IsNil) p1sAfter, err := h.getPeers(m1) c.Assert(err, check.IsNil) - c.Assert(len(*p1sAfter), check.Equals, 1) - c.Assert((*p1sAfter)[0].ID, check.Equals, tailcfg.NodeID(m2.ID)) + c.Assert(len(p1sAfter), check.Equals, 1) + c.Assert(p1sAfter[0].ID, check.Equals, m2.ID) } func (s *Suite) TestSameNamespace(c *check.C) { @@ -84,7 +83,7 @@ func (s *Suite) TestSameNamespace(c *check.C) { _, err = h.GetMachine(n1.Name, "test_get_shared_nodes_1") c.Assert(err, check.NotNil) - m1 := Machine{ + m1 := &Machine{ ID: 0, MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", @@ -96,12 +95,12 @@ func (s *Suite) TestSameNamespace(c *check.C) { IPAddress: "100.64.0.1", AuthKeyID: uint(pak1.ID), } - h.db.Save(&m1) + h.db.Save(m1) _, err = h.GetMachine(n1.Name, m1.Name) c.Assert(err, check.IsNil) - m2 := Machine{ + m2 := &Machine{ ID: 1, MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", @@ -113,16 +112,16 @@ func (s *Suite) TestSameNamespace(c *check.C) { IPAddress: "100.64.0.2", AuthKeyID: uint(pak2.ID), } - h.db.Save(&m2) + h.db.Save(m2) _, err = h.GetMachine(n2.Name, m2.Name) c.Assert(err, check.IsNil) p1s, err := h.getPeers(m1) c.Assert(err, check.IsNil) - c.Assert(len(*p1s), check.Equals, 0) + c.Assert(len(p1s), check.Equals, 0) - err = h.AddSharedMachineToNamespace(&m1, n1) + err = h.AddSharedMachineToNamespace(m1, n1) c.Assert(err, check.Equals, errorSameNamespace) } @@ -142,7 +141,7 @@ func (s *Suite) TestAlreadyShared(c *check.C) { _, err = h.GetMachine(n1.Name, "test_get_shared_nodes_1") c.Assert(err, check.NotNil) - m1 := Machine{ + m1 := &Machine{ ID: 0, MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", @@ -154,12 +153,12 @@ func (s *Suite) TestAlreadyShared(c *check.C) { IPAddress: "100.64.0.1", AuthKeyID: uint(pak1.ID), } - h.db.Save(&m1) + h.db.Save(m1) _, err = h.GetMachine(n1.Name, m1.Name) c.Assert(err, check.IsNil) - m2 := Machine{ + m2 := &Machine{ ID: 1, MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", @@ -171,18 +170,18 @@ func (s *Suite) TestAlreadyShared(c *check.C) { IPAddress: "100.64.0.2", AuthKeyID: uint(pak2.ID), } - h.db.Save(&m2) + h.db.Save(m2) _, err = h.GetMachine(n2.Name, m2.Name) c.Assert(err, check.IsNil) p1s, err := h.getPeers(m1) c.Assert(err, check.IsNil) - c.Assert(len(*p1s), check.Equals, 0) + c.Assert(len(p1s), check.Equals, 0) - err = h.AddSharedMachineToNamespace(&m2, n1) + err = h.AddSharedMachineToNamespace(m2, n1) c.Assert(err, check.IsNil) - err = h.AddSharedMachineToNamespace(&m2, n1) + err = h.AddSharedMachineToNamespace(m2, n1) c.Assert(err, check.Equals, errorMachineAlreadyShared) } @@ -202,7 +201,7 @@ func (s *Suite) TestDoNotIncludeRoutesOnShared(c *check.C) { _, err = h.GetMachine(n1.Name, "test_get_shared_nodes_1") c.Assert(err, check.NotNil) - m1 := Machine{ + m1 := &Machine{ ID: 0, MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", @@ -214,12 +213,12 @@ func (s *Suite) TestDoNotIncludeRoutesOnShared(c *check.C) { IPAddress: "100.64.0.1", AuthKeyID: uint(pak1.ID), } - h.db.Save(&m1) + h.db.Save(m1) _, err = h.GetMachine(n1.Name, m1.Name) c.Assert(err, check.IsNil) - m2 := Machine{ + m2 := &Machine{ ID: 1, MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", @@ -231,22 +230,22 @@ func (s *Suite) TestDoNotIncludeRoutesOnShared(c *check.C) { IPAddress: "100.64.0.2", AuthKeyID: uint(pak2.ID), } - h.db.Save(&m2) + h.db.Save(m2) _, err = h.GetMachine(n2.Name, m2.Name) c.Assert(err, check.IsNil) p1s, err := h.getPeers(m1) c.Assert(err, check.IsNil) - c.Assert(len(*p1s), check.Equals, 0) + c.Assert(len(p1s), check.Equals, 0) - err = h.AddSharedMachineToNamespace(&m2, n1) + err = h.AddSharedMachineToNamespace(m2, n1) c.Assert(err, check.IsNil) p1sAfter, err := h.getPeers(m1) c.Assert(err, check.IsNil) - c.Assert(len(*p1sAfter), check.Equals, 1) - c.Assert(len((*p1sAfter)[0].AllowedIPs), check.Equals, 1) + c.Assert(len(p1sAfter), check.Equals, 1) + c.Assert(p1sAfter[0].Name, check.Equals, "test_get_shared_nodes_2") } func (s *Suite) TestComplexSharingAcrossNamespaces(c *check.C) { @@ -274,7 +273,7 @@ func (s *Suite) TestComplexSharingAcrossNamespaces(c *check.C) { _, err = h.GetMachine(n1.Name, "test_get_shared_nodes_1") c.Assert(err, check.NotNil) - m1 := Machine{ + m1 := &Machine{ ID: 0, MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", @@ -286,12 +285,12 @@ func (s *Suite) TestComplexSharingAcrossNamespaces(c *check.C) { IPAddress: "100.64.0.1", AuthKeyID: uint(pak1.ID), } - h.db.Save(&m1) + h.db.Save(m1) _, err = h.GetMachine(n1.Name, m1.Name) c.Assert(err, check.IsNil) - m2 := Machine{ + m2 := &Machine{ ID: 1, MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", @@ -303,12 +302,12 @@ func (s *Suite) TestComplexSharingAcrossNamespaces(c *check.C) { IPAddress: "100.64.0.2", AuthKeyID: uint(pak2.ID), } - h.db.Save(&m2) + h.db.Save(m2) _, err = h.GetMachine(n2.Name, m2.Name) c.Assert(err, check.IsNil) - m3 := Machine{ + m3 := &Machine{ ID: 2, MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", @@ -320,12 +319,12 @@ func (s *Suite) TestComplexSharingAcrossNamespaces(c *check.C) { IPAddress: "100.64.0.3", AuthKeyID: uint(pak3.ID), } - h.db.Save(&m3) + h.db.Save(m3) _, err = h.GetMachine(n3.Name, m3.Name) c.Assert(err, check.IsNil) - m4 := Machine{ + m4 := &Machine{ ID: 3, MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", @@ -337,23 +336,31 @@ func (s *Suite) TestComplexSharingAcrossNamespaces(c *check.C) { IPAddress: "100.64.0.4", AuthKeyID: uint(pak4.ID), } - h.db.Save(&m4) + h.db.Save(m4) _, err = h.GetMachine(n1.Name, m4.Name) c.Assert(err, check.IsNil) p1s, err := h.getPeers(m1) c.Assert(err, check.IsNil) - c.Assert(len(*p1s), check.Equals, 1) // nodes 1 and 4 + c.Assert(len(p1s), check.Equals, 1) // nodes 1 and 4 + c.Assert(p1s[0].Name, check.Equals, "test_get_shared_nodes_4") - err = h.AddSharedMachineToNamespace(&m2, n1) + err = h.AddSharedMachineToNamespace(m2, n1) c.Assert(err, check.IsNil) p1sAfter, err := h.getPeers(m1) c.Assert(err, check.IsNil) - c.Assert(len(*p1sAfter), check.Equals, 2) // nodes 1, 2, 4 + c.Assert(len(p1sAfter), check.Equals, 2) // nodes 1, 2, 4 + c.Assert(p1sAfter[0].Name, check.Equals, "test_get_shared_nodes_2") + c.Assert(p1sAfter[1].Name, check.Equals, "test_get_shared_nodes_4") + + node1shared, err := h.getShared(m1) + c.Assert(err, check.IsNil) + c.Assert(len(node1shared), check.Equals, 1) // nodes 1, 2, 4 + c.Assert(node1shared[0].Name, check.Equals, "test_get_shared_nodes_2") pAlone, err := h.getPeers(m3) c.Assert(err, check.IsNil) - c.Assert(len(*pAlone), check.Equals, 0) // node 3 is alone + c.Assert(len(pAlone), check.Equals, 0) // node 3 is alone }