Skip to content
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

Prepare and show gRPC usage in CLI #206

Merged
merged 27 commits into from
Nov 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
06700c1
Setup proto linting
kradalby Oct 29, 2021
07bbeaf
Fix lint errors, add initial namespace rpc
kradalby Oct 29, 2021
c7fa9b6
Setup create, delete and list namespace over grpc
kradalby Oct 29, 2021
a23d82e
Setup API and prepare for API keys
kradalby Oct 29, 2021
6d10be8
Change order of print/nil check in integration test
kradalby Oct 29, 2021
68dab0f
Move localhost check to utils
kradalby Oct 29, 2021
002b5c1
Add grpc token auth struct
kradalby Oct 29, 2021
eefd82a
Move config loading out of the headscale app setup
kradalby Oct 29, 2021
81b8610
Add helper function to setup grpc client for cli
kradalby Oct 29, 2021
9ef031f
Port create, delete and list of namespace to grpc
kradalby Oct 29, 2021
72fd2a2
Fix lint error
kradalby Oct 29, 2021
6aacada
Switch from gRPC localhost to socket
kradalby Oct 30, 2021
434fac5
Fix lint error
kradalby Oct 30, 2021
2b340e8
Rename protofile
kradalby Oct 30, 2021
482a31b
Setup swagger and swagger UI properly
kradalby Oct 30, 2021
018e42a
Merge branch 'main' into initial-api-cli-work
kradalby Oct 30, 2021
264e596
Resolve merge conflict
kradalby Oct 31, 2021
ca15a53
Add timeout to integration test for execCommand to fail faster
kradalby Oct 31, 2021
f968b0a
Merge branch 'main' into initial-api-cli-work
kradalby Oct 31, 2021
3f30bf1
Ensure we set up TLS for http
kradalby Oct 31, 2021
12f2a7c
Move context per cure's suggestion
kradalby Oct 31, 2021
1c9b1ea
Add todo
kradalby Oct 31, 2021
8db45a4
Setup a seperate, non-tls, no auth, socket grpc
kradalby Oct 31, 2021
53df9af
Fix step naming error
kradalby Oct 31, 2021
b91c115
Remove "auth skip" for socket traffic
kradalby Oct 31, 2021
9954a3c
Add handling for closing the socket
kradalby Nov 2, 2021
7c774bc
Remove flag that cant be trapped
kradalby Nov 2, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ docker-compose*
README.md
LICENSE
.vscode

*.sock
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
sudo apt update
sudo apt install -y make

- name: Run lint
- name: Run build
run: make build

- uses: actions/upload-artifact@v2
Expand Down
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ coverprofile_html:
lint:
golangci-lint run --fix

proto-lint:
cd proto/ && buf lint

compress: build
upx --brute headscale

Expand Down
211 changes: 195 additions & 16 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import (
"crypto/tls"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"os/signal"
"sort"
"strings"
"sync"
"syscall"
"time"

"github.com/coreos/go-oidc/v3/oidc"
Expand All @@ -20,21 +23,31 @@ import (

"github.com/gin-gonic/gin"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
apiV1 "github.com/juanfont/headscale/gen/go/v1"
apiV1 "github.com/juanfont/headscale/gen/go/headscale/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"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/peer"
"google.golang.org/grpc/reflection"
"google.golang.org/grpc/status"
"gorm.io/gorm"
"inet.af/netaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/dnstype"
"tailscale.com/types/wgkey"
)

const (
AUTH_PREFIX = "Bearer "
)

// Config contains the initial Headscale configuration.
type Config struct {
ServerURL string
Expand Down Expand Up @@ -67,6 +80,8 @@ type Config struct {

DNSConfig *tailcfg.DNSConfig

UnixSocket string

OIDC OIDCConfig

MaxMachineRegistrationDuration time.Duration
Expand Down Expand Up @@ -235,6 +250,96 @@ func (h *Headscale) watchForKVUpdatesWorker() {
// more functions will come here in the future
}

func (h *Headscale) grpcAuthenticationInterceptor(ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler) (interface{}, error) {

// Check if the request is coming from the on-server client.
// This is not secure, but it is to maintain maintainability
// with the "legacy" database-based client
// It is also neede for grpc-gateway to be able to connect to
// the server
p, _ := peer.FromContext(ctx)

log.Trace().Caller().Str("client_address", p.Addr.String()).Msg("Client is trying to authenticate")

md, ok := metadata.FromIncomingContext(ctx)
if !ok {
log.Error().Caller().Str("client_address", p.Addr.String()).Msg("Retrieving metadata is failed")
return ctx, status.Errorf(codes.InvalidArgument, "Retrieving metadata is failed")
}

authHeader, ok := md["authorization"]
if !ok {
log.Error().Caller().Str("client_address", p.Addr.String()).Msg("Authorization token is not supplied")
return ctx, status.Errorf(codes.Unauthenticated, "Authorization token is not supplied")
}

token := authHeader[0]

if !strings.HasPrefix(token, AUTH_PREFIX) {
log.Error().
Caller().
Str("client_address", p.Addr.String()).
Msg(`missing "Bearer " prefix in "Authorization" header`)
return ctx, status.Error(codes.Unauthenticated, `missing "Bearer " prefix in "Authorization" header`)
}

// TODO(kradalby): Implement API key backend:
// - Table in the DB
// - Key name
// - Encrypted
// - Expiry
//
// Currently all other than localhost traffic is unauthorized, this is intentional to allow
// us to make use of gRPC for our CLI, but not having to implement any of the remote capabilities
// and API key auth
return ctx, status.Error(codes.Unauthenticated, "Authentication is not implemented yet")

//if strings.TrimPrefix(token, AUTH_PREFIX) != a.Token {
// log.Error().Caller().Str("client_address", p.Addr.String()).Msg("invalid token")
// return ctx, status.Error(codes.Unauthenticated, "invalid token")
//}

// return handler(ctx, req)
}

func (h *Headscale) httpAuthenticationMiddleware(c *gin.Context) {
log.Trace().
Caller().
Str("client_address", c.ClientIP()).
Msg("HTTP authentication invoked")

authHeader := c.GetHeader("authorization")

if !strings.HasPrefix(authHeader, AUTH_PREFIX) {
log.Error().
Caller().
Str("client_address", c.ClientIP()).
Msg(`missing "Bearer " prefix in "Authorization" header`)
c.AbortWithStatus(http.StatusUnauthorized)

return
}

c.AbortWithStatus(http.StatusUnauthorized)

// TODO(kradalby): Implement API key backend
// Currently all traffic is unauthorized, this is intentional to allow
// us to make use of gRPC for our CLI, but not having to implement any of the remote capabilities
// and API key auth
//
// if strings.TrimPrefix(authHeader, AUTH_PREFIX) != a.Token {
// log.Error().Caller().Str("client_address", c.ClientIP()).Msg("invalid token")
// c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error", "unauthorized"})

// return
// }

// c.Next()
}

// Serve launches a GIN server with the Headscale API.
func (h *Headscale) Serve() error {
var err error
Expand All @@ -244,30 +349,57 @@ func (h *Headscale) Serve() error {

defer cancel()

l, err := net.Listen("tcp", h.cfg.Addr)
socketListener, err := net.Listen("unix", h.cfg.UnixSocket)
if err != nil {
panic(err)
}

// Handle common process-killing signals so we can gracefully shut down:
sigc := make(chan os.Signal, 1)
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
go func(c chan os.Signal) {
// Wait for a SIGINT or SIGKILL:
sig := <-c
log.Printf("Caught signal %s: shutting down.", sig)
// Stop listening (and unlink the socket if unix type):
socketListener.Close()
// And we're done:
os.Exit(0)
}(sigc)

networkListener, 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)
m := cmux.New(networkListener)
// Match gRPC requests here
grpcListener := m.MatchWithWriters(cmux.HTTP2MatchHeaderFieldSendSettings("content-type", "application/grpc"))
grpcListener := m.MatchWithWriters(
cmux.HTTP2MatchHeaderFieldSendSettings("content-type", "application/grpc"),
cmux.HTTP2MatchHeaderFieldSendSettings("content-type", "application/grpc+proto"),
)
// 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()}
// Make the grpc-gateway connect to grpc over socket
grpcGatewayConn, err := grpc.Dial(
h.cfg.UnixSocket,
[]grpc.DialOption{
grpc.WithInsecure(),
grpc.WithContextDialer(GrpcSocketDialer),
}...,
)
if err != nil {
return err
}

err = apiV1.RegisterHeadscaleServiceHandlerFromEndpoint(ctx, grpcGatewayMux, h.cfg.Addr, opts)
// Connect to the gRPC server over localhost to skip
// the authentication.
err = apiV1.RegisterHeadscaleServiceHandler(ctx, grpcGatewayMux, grpcGatewayConn)
if err != nil {
return err
}
Expand All @@ -286,11 +418,16 @@ func (h *Headscale) Serve() error {
r.GET("/oidc/callback", h.OIDCCallback)
r.GET("/apple", h.AppleMobileConfig)
r.GET("/apple/:platform", h.ApplePlatformConfig)
r.GET("/swagger", SwaggerUI)
r.GET("/swagger/v1/openapiv2.json", SwaggerAPIv1)

r.Any("/api/v1/*any", gin.WrapF(grpcGatewayMux.ServeHTTP))
r.StaticFile("/swagger/swagger.json", "gen/openapiv2/v1/headscale.swagger.json")
api := r.Group("/api")
api.Use(h.httpAuthenticationMiddleware)
{
api.Any("/v1/*any", gin.WrapF(grpcGatewayMux.ServeHTTP))
}

updateMillisecondsWait := int64(5000)
r.NoRoute(stdoutHandler)

// Fetch an initial DERP Map before we start serving
h.DERPMap = GetDERPMap(h.cfg.DERP)
Expand All @@ -302,6 +439,7 @@ func (h *Headscale) Serve() error {
}

// I HATE THIS
updateMillisecondsWait := int64(5000)
go h.watchForKVUpdates(updateMillisecondsWait)
go h.expireEphemeralNodes(updateMillisecondsWait)

Expand All @@ -316,6 +454,12 @@ func (h *Headscale) Serve() error {
WriteTimeout: 0,
}

grpcOptions := []grpc.ServerOption{
grpc.UnaryInterceptor(
h.grpcAuthenticationInterceptor,
),
}

tlsConfig, err := h.getTLSSettings()
if err != nil {
log.Error().Err(err).Msg("Failed to set up TLS configuration")
Expand All @@ -325,12 +469,36 @@ func (h *Headscale) Serve() error {

if tlsConfig != nil {
httpServer.TLSConfig = tlsConfig

grpcOptions = append(grpcOptions, grpc.Creds(credentials.NewTLS(tlsConfig)))
}

grpcServer := grpc.NewServer(grpcOptions...)

// Start the local gRPC server without TLS and without authentication
grpcSocket := grpc.NewServer()

apiV1.RegisterHeadscaleServiceServer(grpcServer, newHeadscaleV1APIServer(h))
apiV1.RegisterHeadscaleServiceServer(grpcSocket, newHeadscaleV1APIServer(h))
reflection.Register(grpcServer)
reflection.Register(grpcSocket)

g := new(errgroup.Group)

g.Go(func() error { return grpcSocket.Serve(socketListener) })

// TODO(kradalby): Verify if we need the same TLS setup for gRPC as HTTP
g.Go(func() error { return grpcServer.Serve(grpcListener) })
g.Go(func() error { return httpServer.Serve(httpListener) })

if tlsConfig != nil {
g.Go(func() error {
tlsl := tls.NewListener(httpListener, tlsConfig)
return httpServer.Serve(tlsl)
})
} else {
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)
Expand Down Expand Up @@ -423,3 +591,14 @@ func (h *Headscale) getLastStateChange(namespaces ...string) time.Time {
return times[0]
}
}

func stdoutHandler(c *gin.Context) {
b, _ := io.ReadAll(c.Request.Body)

log.Trace().
Interface("header", c.Request.Header).
Interface("proto", c.Request.Proto).
Interface("url", c.Request.URL).
Bytes("body", b).
Msg("Request did not match")
}
Loading