-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Initial work preparing for API/gRPC #204
Merged
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
b096a2e
Create an initial gRPC service
kradalby a9da7c8
Update go.mod
kradalby caa4d33
Add an initial grpcv1 service (implementing the proto generated service)
kradalby 2f045b2
Refactor tls and wire up grpc, grpc gateway/api
kradalby b8c89cd
Add readme and makefile entry about code generation
kradalby 11d9875
Ignore generated files for docker
kradalby 6e76494
Add grpc step to dockerfile
kradalby 8f2ef6a
Prepare for checking in generated code
kradalby d426577
Check in generated code
kradalby 2d92719
Dont try to generate code on every make build
kradalby 6369cea
Remove golint, its deprecated
kradalby acd9ebb
Let lint ignore grpcv1.go as it is placeholder
kradalby f779372
Add golangcilint config
kradalby c9bd25d
Remove golint from github actions
kradalby e91174e
Add gen explicitly to skip list
kradalby 5054ed4
Make ci lint fix if it can
kradalby File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,4 +14,3 @@ docker-compose* | |
README.md | ||
LICENSE | ||
.vscode | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
--- | ||
run: | ||
timeout: 5m | ||
|
||
issues: | ||
skip-dirs: | ||
- gen |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,11 @@ | ||
package headscale | ||
|
||
import ( | ||
"context" | ||
"crypto/tls" | ||
"errors" | ||
"fmt" | ||
"net" | ||
"net/http" | ||
"net/url" | ||
"os" | ||
|
@@ -11,20 +14,24 @@ import ( | |
"sync" | ||
"time" | ||
|
||
"github.com/rs/zerolog/log" | ||
|
||
"github.com/gin-gonic/gin" | ||
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime" | ||
apiV1 "github.com/juanfont/headscale/gen/go/v1" | ||
"github.com/rs/zerolog/log" | ||
"github.com/soheilhy/cmux" | ||
ginprometheus "github.com/zsais/go-gin-prometheus" | ||
"golang.org/x/crypto/acme" | ||
"golang.org/x/crypto/acme/autocert" | ||
"golang.org/x/sync/errgroup" | ||
"google.golang.org/grpc" | ||
"gorm.io/gorm" | ||
"inet.af/netaddr" | ||
"tailscale.com/tailcfg" | ||
"tailscale.com/types/dnstype" | ||
"tailscale.com/types/wgkey" | ||
) | ||
|
||
// Config contains the initial Headscale configuration | ||
// Config contains the initial Headscale configuration. | ||
type Config struct { | ||
ServerURL string | ||
Addr string | ||
|
@@ -64,7 +71,7 @@ type DERPConfig struct { | |
UpdateFrequency time.Duration | ||
} | ||
|
||
// Headscale represents the base app of the service | ||
// Headscale represents the base app of the service. | ||
type Headscale struct { | ||
cfg Config | ||
db *gorm.DB | ||
|
@@ -82,12 +89,13 @@ type Headscale struct { | |
lastStateChange sync.Map | ||
} | ||
|
||
// NewHeadscale returns the Headscale app | ||
// NewHeadscale returns the Headscale app. | ||
func NewHeadscale(cfg Config) (*Headscale, error) { | ||
content, err := os.ReadFile(cfg.PrivateKeyPath) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
privKey, err := wgkey.ParsePrivate(string(content)) | ||
if err != nil { | ||
return nil, err | ||
|
@@ -136,14 +144,14 @@ func NewHeadscale(cfg Config) (*Headscale, error) { | |
return &h, nil | ||
} | ||
|
||
// Redirect to our TLS url | ||
// Redirect to our TLS url. | ||
func (h *Headscale) redirect(w http.ResponseWriter, req *http.Request) { | ||
target := h.cfg.ServerURL + req.URL.RequestURI() | ||
http.Redirect(w, req, target, http.StatusFound) | ||
} | ||
|
||
// expireEphemeralNodes deletes ephemeral machine records that have not been | ||
// seen for longer than h.cfg.EphemeralNodeInactivityTimeout | ||
// seen for longer than h.cfg.EphemeralNodeInactivityTimeout. | ||
func (h *Headscale) expireEphemeralNodes(milliSeconds int64) { | ||
ticker := time.NewTicker(time.Duration(milliSeconds) * time.Millisecond) | ||
for range ticker.C { | ||
|
@@ -155,18 +163,23 @@ func (h *Headscale) expireEphemeralNodesWorker() { | |
namespaces, err := h.ListNamespaces() | ||
if err != nil { | ||
log.Error().Err(err).Msg("Error listing namespaces") | ||
|
||
return | ||
} | ||
|
||
for _, ns := range *namespaces { | ||
machines, err := h.ListMachinesInNamespace(ns.Name) | ||
if err != nil { | ||
log.Error().Err(err).Str("namespace", ns.Name).Msg("Error listing machines in namespace") | ||
|
||
return | ||
} | ||
|
||
for _, m := range *machines { | ||
if m.AuthKey != nil && m.LastSeen != nil && m.AuthKey.Ephemeral && | ||
time.Now().After(m.LastSeen.Add(h.cfg.EphemeralNodeInactivityTimeout)) { | ||
log.Info().Str("machine", m.Name).Msg("Ephemeral client removed from database") | ||
|
||
err = h.db.Unscoped().Delete(m).Error | ||
if err != nil { | ||
log.Error(). | ||
|
@@ -176,12 +189,13 @@ func (h *Headscale) expireEphemeralNodesWorker() { | |
} | ||
} | ||
} | ||
|
||
h.setLastStateChangeToNow(ns.Name) | ||
} | ||
} | ||
|
||
// WatchForKVUpdates checks the KV DB table for requests to perform tailnet upgrades | ||
// This is a way to communitate the CLI with the headscale server | ||
// 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 { | ||
|
@@ -194,24 +208,60 @@ func (h *Headscale) watchForKVUpdatesWorker() { | |
// 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 { | ||
var err error | ||
|
||
ctx := context.Background() | ||
ctx, cancel := context.WithCancel(ctx) | ||
|
||
defer cancel() | ||
|
||
l, err := net.Listen("tcp", h.cfg.Addr) | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
// Create the cmux object that will multiplex 2 protocols on the same port. | ||
// The two following listeners will be served on the same port below gracefully. | ||
m := cmux.New(l) | ||
// Match gRPC requests here | ||
grpcListener := m.MatchWithWriters(cmux.HTTP2MatchHeaderFieldSendSettings("content-type", "application/grpc")) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Noice. And clever. |
||
// Otherwise match regular http requests. | ||
httpListener := m.Match(cmux.Any()) | ||
|
||
// Now create the grpc server with those options. | ||
grpcServer := grpc.NewServer() | ||
|
||
// TODO(kradalby): register the new server when we have authentication ready | ||
// apiV1.RegisterHeadscaleServiceServer(grpcServer, newHeadscaleV1APIServer(h)) | ||
|
||
grpcGatewayMux := runtime.NewServeMux() | ||
|
||
opts := []grpc.DialOption{grpc.WithInsecure()} | ||
|
||
err = apiV1.RegisterHeadscaleServiceHandlerFromEndpoint(ctx, grpcGatewayMux, h.cfg.Addr, opts) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
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("/health", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"healthy": "ok"}) }) | ||
r.GET("/key", h.KeyHandler) | ||
r.GET("/register", h.RegisterWebAPI) | ||
r.POST("/machine/:id/map", h.PollNetMapHandler) | ||
r.POST("/machine/:id", h.RegistrationHandler) | ||
r.GET("/apple", h.AppleMobileConfig) | ||
r.GET("/apple/:platform", h.ApplePlatformConfig) | ||
var err error | ||
|
||
go h.watchForKVUpdates(5000) | ||
go h.expireEphemeralNodes(5000) | ||
r.Any("/api/v1/*any", gin.WrapF(grpcGatewayMux.ServeHTTP)) | ||
r.StaticFile("/swagger/swagger.json", "gen/openapiv2/v1/headscale.swagger.json") | ||
|
||
updateMillisecondsWait := int64(5000) | ||
|
||
// Fetch an initial DERP Map before we start serving | ||
h.DERPMap = GetDERPMap(h.cfg.DERP) | ||
|
@@ -222,7 +272,11 @@ func (h *Headscale) Serve() error { | |
go h.scheduledDERPMapUpdateWorker(derpMapCancelChannel) | ||
} | ||
|
||
s := &http.Server{ | ||
// I HATE THIS | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Me too. |
||
go h.watchForKVUpdates(updateMillisecondsWait) | ||
go h.expireEphemeralNodes(updateMillisecondsWait) | ||
|
||
httpServer := &http.Server{ | ||
Addr: h.cfg.Addr, | ||
Handler: r, | ||
ReadTimeout: 30 * time.Second, | ||
|
@@ -233,6 +287,29 @@ func (h *Headscale) Serve() error { | |
WriteTimeout: 0, | ||
} | ||
|
||
tlsConfig, err := h.getTLSSettings() | ||
if err != nil { | ||
log.Error().Err(err).Msg("Failed to set up TLS configuration") | ||
|
||
return err | ||
} | ||
|
||
if tlsConfig != nil { | ||
httpServer.TLSConfig = tlsConfig | ||
} | ||
|
||
g := new(errgroup.Group) | ||
|
||
g.Go(func() error { return grpcServer.Serve(grpcListener) }) | ||
g.Go(func() error { return httpServer.Serve(httpListener) }) | ||
g.Go(func() error { return m.Serve() }) | ||
|
||
log.Info().Msgf("listening and serving (multiplexed HTTP and gRPC) on: %s", h.cfg.Addr) | ||
|
||
return g.Wait() | ||
} | ||
|
||
func (h *Headscale) getTLSSettings() (*tls.Config, error) { | ||
if h.cfg.TLSLetsEncryptHostname != "" { | ||
if !strings.HasPrefix(h.cfg.ServerURL, "https://") { | ||
log.Warn().Msg("Listening with TLS but ServerURL does not start with https://") | ||
|
@@ -248,13 +325,11 @@ func (h *Headscale) Serve() error { | |
Email: h.cfg.ACMEEmail, | ||
} | ||
|
||
s.TLSConfig = m.TLSConfig() | ||
|
||
if h.cfg.TLSLetsEncryptChallengeType == "TLS-ALPN-01" { | ||
// Configuration via autocert with TLS-ALPN-01 (https://tools.ietf.org/html/rfc8737) | ||
// The RFC requires that the validation is done on port 443; in other words, headscale | ||
// must be reachable on port 443. | ||
err = s.ListenAndServeTLS("", "") | ||
return m.TLSConfig(), nil | ||
} else if h.cfg.TLSLetsEncryptChallengeType == "HTTP-01" { | ||
// Configuration via autocert with HTTP-01. This requires listening on | ||
// port 80 for the certificate validation in addition to the headscale | ||
|
@@ -264,22 +339,30 @@ func (h *Headscale) Serve() error { | |
Err(http.ListenAndServe(h.cfg.TLSLetsEncryptListen, m.HTTPHandler(http.HandlerFunc(h.redirect)))). | ||
Msg("failed to set up a HTTP server") | ||
}() | ||
err = s.ListenAndServeTLS("", "") | ||
|
||
return m.TLSConfig(), nil | ||
} else { | ||
return errors.New("unknown value for TLSLetsEncryptChallengeType") | ||
return nil, errors.New("unknown value for TLSLetsEncryptChallengeType") | ||
} | ||
} else if h.cfg.TLSCertPath == "" { | ||
if !strings.HasPrefix(h.cfg.ServerURL, "http://") { | ||
log.Warn().Msg("Listening without TLS but ServerURL does not start with http://") | ||
} | ||
err = s.ListenAndServe() | ||
|
||
return nil, nil | ||
} else { | ||
if !strings.HasPrefix(h.cfg.ServerURL, "https://") { | ||
log.Warn().Msg("Listening with TLS but ServerURL does not start with https://") | ||
} | ||
err = s.ListenAndServeTLS(h.cfg.TLSCertPath, h.cfg.TLSKeyPath) | ||
var err error | ||
tlsConfig := &tls.Config{} | ||
tlsConfig.ClientAuth = tls.RequireAnyClientCert | ||
tlsConfig.NextProtos = []string{"http/1.1"} | ||
tlsConfig.Certificates = make([]tls.Certificate, 1) | ||
tlsConfig.Certificates[0], err = tls.LoadX509KeyPair(h.cfg.TLSCertPath, h.cfg.TLSKeyPath) | ||
|
||
return tlsConfig, err | ||
} | ||
return err | ||
} | ||
|
||
func (h *Headscale) setLastStateChangeToNow(namespace string) { | ||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Noice, an omnibus PR