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

Serve Management gRPC and HTTP on a single 80/443 port #400

Merged
merged 11 commits into from
Jul 29, 2022
2 changes: 1 addition & 1 deletion client/internal/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func ManagementURLDefault() *url.URL {
}

func init() {
managementURL, err := parseURL("Management URL", "https://api.wiretrustee.com:33073")
managementURL, err := parseURL("Management URL", "https://api.wiretrustee.com:443")
if err != nil {
panic(err)
}
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ require (
github.com/rs/xid v1.3.0
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/stretchr/testify v1.7.1
golang.org/x/net v0.0.0-20220513224357-95641704303c
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467
)

Expand Down Expand Up @@ -96,7 +97,6 @@ require (
github.com/yuin/goldmark v1.4.1 // indirect
golang.org/x/image v0.0.0-20200430140353-33d19683fad8 // indirect
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
golang.org/x/net v0.0.0-20220513224357-95641704303c // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/text v0.3.8-0.20211105212822-18b340fc7af2 // indirect
golang.org/x/tools v0.1.10 // indirect
Expand Down
247 changes: 181 additions & 66 deletions management/cmd/management.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
package cmd

import (
"context"
"crypto/tls"
"errors"
"flag"
"fmt"
httpapi "github.com/netbirdio/netbird/management/server/http"
"golang.org/x/crypto/acme/autocert"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
"io"
"io/fs"
"io/ioutil"
"net"
"net/http"
"os"
"path"
"strings"
"time"

"github.com/netbirdio/netbird/management/server"
"github.com/netbirdio/netbird/management/server/http"
"github.com/netbirdio/netbird/management/server/idp"
"github.com/netbirdio/netbird/util"

Expand All @@ -28,11 +32,16 @@ import (
"google.golang.org/grpc/keepalive"
)

// ManagementLegacyPort is the port that was used before by the Management gRPC server.
// It is used for backward compatibility now.
const ManagementLegacyPort = 33073

var (
mgmtPort int
mgmtLetsencryptDomain string
certFile string
certKey string
config *server.Config

kaep = keepalive.EnforcementPolicy{
MinTime: 15 * time.Second,
Expand All @@ -46,133 +55,239 @@ var (
Timeout: 2 * time.Second,
}

// TLS enabled:
// - HTTP 80 for LetsEncrypt
// - if --port not specified gRPC and HTTP servers on 443 (with multiplexing)
// - if --port=X specified then run gRPC and HTTP servers on X (with multiplexing)
// - if --port=80 forbid this (throw error, otherwise we need to overcomplicate the logic with multiplexing)
// TLS disabled:
// - if --port not specified gRPC and HTTP servers on 443 on 80 (with multiplexing)
// - if --port=X specified then run gRPC and HTTP servers on 443 on X (with multiplexing)
// Always run gRPC on port 33073 regardless of TLS to be backward compatible
// Remove HTTP port 33071 from the configuration.

mgmtCmd = &cobra.Command{
Use: "management",
Short: "start Netbird Management Server",
Run: func(cmd *cobra.Command, args []string) {
flag.Parse()
err := util.InitLog(logLevel, logFile)
Short: "start NetBird Management Server",
PreRunE: func(cmd *cobra.Command, args []string) error {
// detect whether user specified a port
userPort := cmd.Flag("port").Changed

var err error
config, err = loadMgmtConfig(mgmtConfig)
if err != nil {
log.Fatalf("failed initializing log %v", err)
return fmt.Errorf("failed reading provided config file: %s: %v", mgmtConfig, err)
}

err = handleRebrand(cmd)
tlsEnabled := false
if mgmtLetsencryptDomain != "" || (config.HttpConfig.CertFile != "" && config.HttpConfig.CertKey != "") {
tlsEnabled = true
}

if !userPort {
// different defaults for port when tls enabled/disabled
if tlsEnabled {
mgmtPort = 443
} else {
mgmtPort = 80
}
}

return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
flag.Parse()
err := util.InitLog(logLevel, logFile)
if err != nil {
log.Fatalf("failed to migrate files %v", err)
return fmt.Errorf("failed initializing log %v", err)
}

config, err := loadMgmtConfig(mgmtConfig)
err = handleRebrand(cmd)
if err != nil {
log.Fatalf("failed reading provided config file: %s: %v", mgmtConfig, err)
return fmt.Errorf("failed to migrate files %v", err)
}

if _, err = os.Stat(config.Datadir); os.IsNotExist(err) {
err = os.MkdirAll(config.Datadir, os.ModeDir)
if err != nil {
log.Fatalf("failed creating datadir: %s: %v", config.Datadir, err)
return fmt.Errorf("failed creating datadir: %s: %v", config.Datadir, err)
}
}

store, err := server.NewStore(config.Datadir)
if err != nil {
log.Fatalf("failed creating a store: %s: %v", config.Datadir, err)
return fmt.Errorf("failed creating Store: %s: %v", config.Datadir, err)
}
peersUpdateManager := server.NewPeersUpdateManager()

var idpManager idp.Manager
if config.IdpManagerConfig != nil {
idpManager, err = idp.NewManager(*config.IdpManagerConfig)
if err != nil {
log.Fatalln("failed retrieving a new idp manager with err: ", err)
return fmt.Errorf("failed retrieving a new idp manager with err: %v", err)
}
}

accountManager, err := server.BuildManager(store, peersUpdateManager, idpManager)
if err != nil {
log.Fatalln("failed build default manager: ", err)
return fmt.Errorf("failed to build default manager: %v", err)
}

var opts []grpc.ServerOption
turnManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig)

var httpServer *http.Server
gRPCOpts := []grpc.ServerOption{grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp)}
var certManager *autocert.Manager
var tlsConfig *tls.Config
tlsEnabled := false
if config.HttpConfig.LetsEncryptDomain != "" {
// automatically generate a new certificate with Let's Encrypt
certManager, err := encryption.CreateCertManager(config.Datadir, config.HttpConfig.LetsEncryptDomain)
certManager, err = encryption.CreateCertManager(config.Datadir, config.HttpConfig.LetsEncryptDomain)
if err != nil {
log.Fatalf("failed creating Let's Encrypt cert manager: %v", err)
return fmt.Errorf("failed creating LetsEncrypt cert manager: %v", err)
}
transportCredentials := credentials.NewTLS(certManager.TLSConfig())
opts = append(opts, grpc.Creds(transportCredentials))

httpServer = http.NewHttpsServer(config.HttpConfig, certManager, accountManager)
gRPCOpts = append(gRPCOpts, grpc.Creds(transportCredentials))
tlsEnabled = true
} else if config.HttpConfig.CertFile != "" && config.HttpConfig.CertKey != "" {
// use provided certificate
tlsConfig, err := loadTLSConfig(config.HttpConfig.CertFile, config.HttpConfig.CertKey)
tlsConfig, err = loadTLSConfig(config.HttpConfig.CertFile, config.HttpConfig.CertKey)
if err != nil {
log.Fatal("cannot load TLS credentials: ", err)
log.Errorf("cannot load TLS credentials: %v", err)
return err
}
transportCredentials := credentials.NewTLS(tlsConfig)
opts = append(opts, grpc.Creds(transportCredentials))
httpServer = http.NewHttpsServerWithTLSConfig(config.HttpConfig, tlsConfig, accountManager)
} else {
// start server without SSL
httpServer = http.NewHttpServer(config.HttpConfig, accountManager)
gRPCOpts = append(gRPCOpts, grpc.Creds(transportCredentials))
tlsEnabled = true
}

opts = append(opts, grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp))
grpcServer := grpc.NewServer(opts...)
turnManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig)
server, err := server.NewServer(config, accountManager, peersUpdateManager, turnManager)
httpAPIHandler, err := httpapi.APIHandler(accountManager,
config.HttpConfig.AuthIssuer, config.HttpConfig.AuthAudience, config.HttpConfig.AuthKeysLocation)
if err != nil {
log.Fatalf("failed creating new server: %v", err)
return fmt.Errorf("failed creating HTTP API handler: %v", err)
}
mgmtProto.RegisterManagementServiceServer(grpcServer, server)
log.Printf("started server: localhost:%v", mgmtPort)

lis, err := net.Listen("tcp", fmt.Sprintf(":%d", mgmtPort))
gRPCAPIHandler := grpc.NewServer(gRPCOpts...)
srv, err := server.NewServer(config, accountManager, peersUpdateManager, turnManager)
if err != nil {
log.Fatalf("failed to listen: %v", err)
return fmt.Errorf("failed creating gRPC API handler: %v", err)
}
mgmtProto.RegisterManagementServiceServer(gRPCAPIHandler, srv)

go func() {
if err = grpcServer.Serve(lis); err != nil {
log.Fatalf("failed to serve gRpc server: %v", err)
var compatListener net.Listener
if mgmtPort != ManagementLegacyPort {
// The Management gRPC server was running on port 33073 previously. Old agents that are already connected to it
// are using port 33073. For compatibility purposes we keep running a 2nd gRPC server on port 33073.
compatListener, err = serveGRPC(gRPCAPIHandler, ManagementLegacyPort)
if err != nil {
return err
}
}()
log.Infof("running gRPC backward compatibility server: %s", compatListener.Addr().String())
}

go func() {
err = httpServer.Start()
rootHandler := handlerFunc(gRPCAPIHandler, httpAPIHandler)
var listener net.Listener
if certManager != nil {
// a call to certManager.Listener() always creates a new listener so we do it once
cml := certManager.Listener()
if mgmtPort == 443 {
// CertManager, HTTP and gRPC API all on the same port
rootHandler = certManager.HTTPHandler(rootHandler)
listener = cml
} else {
listener, err = tls.Listen("tcp", fmt.Sprintf(":%d", mgmtPort), certManager.TLSConfig())
if err != nil {
return fmt.Errorf("failed creating TLS listener on port %d: %v", mgmtPort, err)
}
log.Infof("running HTTP server (LetsEncrypt challenge handler): %s", cml.Addr().String())
serveHTTP(cml, certManager.HTTPHandler(nil))
}
} else if tlsConfig != nil {
listener, err = tls.Listen("tcp", fmt.Sprintf(":%d", mgmtPort), tlsConfig)
if err != nil {
return fmt.Errorf("failed creating TLS listener on port %d: %v", mgmtPort, err)
}
} else {
listener, err = net.Listen("tcp", fmt.Sprintf(":%d", mgmtPort))
if err != nil {
log.Fatalf("failed to serve http server: %v", err)
return fmt.Errorf("failed creating TCP listener on port %d: %v", mgmtPort, err)
}
}()
}

log.Infof("running HTTP server and gRPC server on the same port: %s", listener.Addr().String())
serveGRPCWithHTTP(listener, rootHandler, tlsEnabled)

SetupCloseHandler()

<-stopCh
log.Println("Receive signal to stop running Management server")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
err = httpServer.Stop(ctx)
if err != nil {
log.Fatalf("failed stopping the http server %v", err)
_ = listener.Close()
if certManager != nil {
_ = certManager.Listener().Close()
}
gRPCAPIHandler.Stop()
log.Infof("stopped Management Service")

grpcServer.Stop()
return nil
},
}
)

func notifyStop(msg string) {
select {
case stopCh <- 1:
log.Error(msg)
default:
// stop has been already called, nothing to report
}
}

func serveGRPC(grpcServer *grpc.Server, port int) (net.Listener, error) {
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
return nil, err
}
go func() {
err := grpcServer.Serve(listener)
if err != nil {
notifyStop(fmt.Sprintf("failed running gRPC server on port %d: %v", port, err))
}
}()
return listener, nil
}

func serveHTTP(httpListener net.Listener, handler http.Handler) {
go func() {
err := http.Serve(httpListener, handler)
if err != nil {
notifyStop(fmt.Sprintf("failed running HTTP server: %v", err))
}
}()
}

func serveGRPCWithHTTP(listener net.Listener, handler http.Handler, tlsEnabled bool) {
go func() {
var err error
if tlsEnabled {
err = http.Serve(listener, handler)
} else {
// the following magic is needed to support HTTP2 without TLS
// and still share a single port between gRPC and HTTP APIs
h1s := &http.Server{
Handler: h2c.NewHandler(handler, &http2.Server{}),
}
err = h1s.Serve(listener)
}

if err != nil {
select {
case stopCh <- 1:
log.Errorf("failed to serve HTTP and gRPC server: %v", err)
default:
// stop has been already called, nothing to report
}
}
}()
}

func handlerFunc(gRPCHandler *grpc.Server, httpHandler http.Handler) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
grpcHeader := strings.HasPrefix(request.Header.Get("Content-Type"), "application/grpc") ||
strings.HasPrefix(request.Header.Get("Content-Type"), "application/grpc+proto")
fmt.Println(grpcHeader)
if request.ProtoMajor == 2 && grpcHeader {
gRPCHandler.ServeHTTP(writer, request)
} else {
httpHandler.ServeHTTP(writer, request)
}
})
}

func loadMgmtConfig(mgmtConfigPath string) (*server.Config, error) {
config := &server.Config{}
_, err := util.ReadJson(mgmtConfigPath, config)
Expand Down
2 changes: 1 addition & 1 deletion management/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func init() {
oldDefaultMgmtConfig = oldDefaultMgmtConfigDir + "/management.json"
oldDefaultLogFile = oldDefaultLogDir + "/management.log"

mgmtCmd.Flags().IntVar(&mgmtPort, "port", 33073, "server port to listen on")
mgmtCmd.Flags().IntVar(&mgmtPort, "port", 80, "server port to listen on (defaults to 443 if TLS is enabled, 80 otherwise")
mgmtCmd.Flags().StringVar(&mgmtDataDir, "datadir", defaultMgmtDataDir, "server data directory location")
mgmtCmd.Flags().StringVar(&mgmtConfig, "config", defaultMgmtConfig, "Netbird config file location. Config params specified via command line (e.g. datadir) have a precedence over configuration from this file")
mgmtCmd.Flags().StringVar(&mgmtLetsencryptDomain, "letsencrypt-domain", "", "a domain to issue Let's Encrypt certificate for. Enables TLS using Let's Encrypt. Will fetch and renew certificate, and run the server with TLS")
Expand Down
1 change: 0 additions & 1 deletion management/server/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ type HttpServerConfig struct {
CertFile string
//CertKey is the location of the certificate private key
CertKey string
Address string
// AuthAudience identifies the recipients that the JWT is intended for (aud in JWT)
AuthAudience string
// AuthIssuer identifies principal that issued the JWT.
Expand Down
Loading