Skip to content

Commit

Permalink
Merge branch 'main' into cli-improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
cure authored Oct 29, 2021
2 parents f9187bd + 2c071a8 commit b2d2d56
Show file tree
Hide file tree
Showing 19 changed files with 1,626 additions and 48 deletions.
1 change: 0 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,3 @@ docker-compose*
README.md
LICENSE
.vscode

19 changes: 0 additions & 19 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,3 @@ jobs:
# below, but it's still much faster in the end than installing
# golangci-lint manually in the `Run lint` step.
- uses: golangci/golangci-lint-action@v2
with:
args: --timeout 5m

# Setup Go
- name: Setup Go
uses: actions/setup-go@v2
with:
go-version: "1.16.3" # The Go version to download (if necessary) and use.

# Install all the dependencies
- name: Install dependencies
run: |
go version
go install golang.org/x/lint/golint@latest
sudo apt update
sudo apt install -y make
- name: Run lint
run: make lint
7 changes: 7 additions & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
run:
timeout: 5m

issues:
skip-dirs:
- gen
6 changes: 4 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
FROM bufbuild/buf:1.0.0-rc6 as buf

FROM golang:1.17.1-bullseye AS build
ENV GOPATH /go
WORKDIR /go/src/headscale

COPY go.mod go.sum /go/src/headscale/
WORKDIR /go/src/headscale
RUN go mod download

COPY . /go/src/headscale
COPY . .

RUN go install -a -ldflags="-extldflags=-static" -tags netgo,sqlite_omit_load_extension ./cmd/headscale
RUN test -e /go/bin/headscale
Expand Down
13 changes: 11 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,18 @@ coverprofile_html:
go tool cover -html=coverage.out

lint:
golint
golangci-lint run --timeout 5m
golangci-lint run --fix

compress: build
upx --brute headscale

generate:
rm -rf gen
buf generate proto

install-protobuf-plugins:
go install \
github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway \
github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2 \
google.golang.org/protobuf/cmd/protoc-gen-go \
google.golang.org/grpc/cmd/protoc-gen-go-grpc
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,40 @@ Please have a look at the documentation under [`docs/`](docs/).
1. We have nothing to do with Tailscale, or Tailscale Inc.
2. The purpose of writing this was to learn how Tailscale works.

## Contributing

To contribute to Headscale you would need the lastest version of [Go](golang.org) and [Buf](https://buf.build)(Protobuf generator).

### Install development tools

- Go
- Buf
- Protobuf tools:

```shell
make install-protobuf-plugins
```

### Testing and building

Some parts of the project requires the generation of Go code from Protobuf (if changes is made in `proto/`) and it must be (re-)generated with:

```shell
make generate
```
**Note**: Please check in changes from `gen/` in a separate commit to make it easier to review.

To run the tests:

```shell
make test
```

To build the program:

```shell
make build
```

## Contributors

Expand Down
127 changes: 105 additions & 22 deletions app.go
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"
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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().
Expand All @@ -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 {
Expand All @@ -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"))
// 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)
Expand All @@ -222,7 +272,11 @@ func (h *Headscale) Serve() error {
go h.scheduledDERPMapUpdateWorker(derpMapCancelChannel)
}

s := &http.Server{
// I HATE THIS
go h.watchForKVUpdates(updateMillisecondsWait)
go h.expireEphemeralNodes(updateMillisecondsWait)

httpServer := &http.Server{
Addr: h.cfg.Addr,
Handler: r,
ReadTimeout: 30 * time.Second,
Expand All @@ -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://")
Expand All @@ -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
Expand All @@ -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) {
Expand Down
Loading

0 comments on commit b2d2d56

Please sign in to comment.