Skip to content

Commit

Permalink
feat: listenGoServe method for server, simplify landlock network rules
Browse files Browse the repository at this point in the history
 BREAKING-CHANGE: server.RunTillWaitGroupFinishes has been replaced by a combination of the ListenGoServe method of server.Server and server.ShutdownAfterWaitGroup

thanks to @gnoack for suggesting to apply landlock network restrictions after the net.Listener have started (issue #15)
  • Loading branch information
ngergs committed Oct 14, 2024
1 parent 42832ce commit 1eee590
Show file tree
Hide file tree
Showing 5 changed files with 32 additions and 37 deletions.
45 changes: 22 additions & 23 deletions cmd/websrv/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,17 @@ import (
)

func main() {
ll := landlock.V5.BestEffort()
conf, err := readConfig()
if err != nil {
log.Fatal().Err(err).Msg("Error reading configuration: See https://github.com/ngergs/websrv/config.yaml for the expected structure.")
}

targetDir, err := setup(conf)
if err != nil {
log.Fatal().Err(err).Msg("Error during initialization")
}
if err := setupLandlock(targetDir, conf); err != nil {
if err := landlockFsReadonlyDir(ll, targetDir); err != nil {
log.Fatal().Err(err).Msg("")
}
var wg sync.WaitGroup
Expand Down Expand Up @@ -133,9 +135,16 @@ func main() {
)
log.Info().Msgf("Starting healthcheck server on port %d", conf.Port.Health)
healthCtx := context.WithValue(context.Background(), server.ServerName, "health server")
healthServer.ListenGoServe(errChan)
if err := landlockNetwork(ll); err != nil {
log.Fatal().Err(err).Msg("")
}
// 1 second is sufficient for health checks to shut down
server.RunTillWaitGroupFinishes(healthCtx, &wg, healthServer, errChan, time.Duration(1)*time.Second)
errChan <- server.ShutdownAfterWaitGroup(healthCtx, &wg, healthServer.Server, time.Duration(1)*time.Second)
} else {
if err := landlockNetwork(ll); err != nil {
log.Fatal().Err(err).Msg("")
}
wg.Wait()
}
}
Expand Down Expand Up @@ -176,28 +185,18 @@ func logErrors(errChan <-chan error) {
}
}

// setupLandlock activates the linux landlock sandbox features on an best effort basis
func setupLandlock(targetDir string, conf *config) error {
llConf := landlock.V4.BestEffort()
if err := llConf.RestrictPaths(landlock.RODirs(targetDir)); err != nil {
return fmt.Errorf("error during landlock filesystem setup: %w", err)
}
ports := []uint16{conf.Port.Webserver}
if conf.Health {
ports = append(ports, conf.Port.Health)
}
if conf.H2C {
ports = append(ports, conf.Port.H2c)
// landlockReadonlyDir restricts file system access to only readonly permissions for the specified directory
func landlockFsReadonlyDir(ll landlock.Config, target string) error {
if err := ll.RestrictPaths(landlock.RODirs(target)); err != nil {
return fmt.Errorf("error during landlock filesystem restriction: %w", err)
}
if conf.Metrics.Enabled {
ports = append(ports, conf.Port.Metrics)
}
portRules := make([]landlock.Rule, len(ports))
for i, port := range ports {
portRules[i] = landlock.BindTCP(port)
}
if err := llConf.RestrictNet(portRules...); err != nil {
return fmt.Errorf("error during landlock network setup: %w", err)
return nil
}

// landlockNetwork allows no additional tcp connections (connect and bind TCP)
func landlockNetwork(ll landlock.Config) error {
if err := ll.RestrictNet(); err != nil {
return fmt.Errorf("error during landlock network restriction: %w", err)
}
return nil
}
4 changes: 2 additions & 2 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ port:
webserver: 8080
# TCP port for the health endpoint
health: 8081
# TCP port for the prometheus metrocs
# TCP port for the prometheus metrics
metrics: 9090
# TCP port for h2c (unecncrypted http2)
# TCP port for h2c (unencrypted http2)
h2c: 443

# the configuration for gzip compression handling
Expand Down
2 changes: 1 addition & 1 deletion example/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,4 @@ angularcsp:
variable: random_csp_nonce
sessioncookie:
name: Nonce-Id
maxage: 600
maxage: 600
14 changes: 5 additions & 9 deletions server/gracefulShutdown.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,15 @@ func AddGracefulShutdown(ctx context.Context, wg *sync.WaitGroup, shutdowner Shu
}()
}

// RunTillWaitGroupFinishes runs the server argument until the WaitGroup wg finishes.
// Subsequently, a graceful shutdown with the given timeout argument is executed.
// Blocks till then.
func RunTillWaitGroupFinishes(ctx context.Context, wg *sync.WaitGroup, server *http.Server, errChan chan<- error, timeout time.Duration) {
go func() { errChan <- server.ListenAndServe() }()
// ShutdownAfterWaitGroup shutdown the server argument after the WaitGroup wg finished
// via a graceful shutdown with the given timeout argument is executed. Blocks till then.
func ShutdownAfterWaitGroup(ctx context.Context, wg *sync.WaitGroup, server *http.Server, timeout time.Duration) error {
wg.Wait()
logShutdown(ctx, timeout)
shutdownCtx, cancel := context.WithDeadline(ctx, time.Now().Add(timeout))
defer cancel()
err := server.Shutdown(shutdownCtx)
if err != nil {
errChan <- err
}
return server.Shutdown(shutdownCtx)

}

// logShutdown logs the relevant info for the shutdown and extracts the optional server name from the context
Expand Down
4 changes: 2 additions & 2 deletions server/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ type Server struct {
*http.Server
}

// ListenGoServe is a half-asynchronous version of ListenAnServe.
// This blocks till net.Listen has returned, the actual Serve of the http.Server is done in an goroutine.
// ListenGoServe is a half-asynchronous version of ListenAnDServe from http.Server.
// This blocks till net.Listen has returned, the actual Serve of the http.Server is done in a separate (automatically spawned) goroutine.
// All errors are returned via the error channel.
func (s *Server) ListenGoServe(errChan chan<- error) {
addr := s.Addr
Expand Down

0 comments on commit 1eee590

Please sign in to comment.