allow pomerium to be embedded as a library (#3415)

This commit is contained in:
Denis Mishin 2022-06-15 20:29:19 -04:00 committed by GitHub
parent 6e1ebffc59
commit d1037d784a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 292 additions and 207 deletions

View file

@ -0,0 +1,227 @@
// Package pomerium houses the main pomerium CLI command.
//
package pomerium
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
envoy_service_auth_v3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
"golang.org/x/sync/errgroup"
"github.com/pomerium/pomerium/authenticate"
"github.com/pomerium/pomerium/authorize"
"github.com/pomerium/pomerium/config"
databroker_service "github.com/pomerium/pomerium/databroker"
"github.com/pomerium/pomerium/internal/autocert"
"github.com/pomerium/pomerium/internal/controlplane"
"github.com/pomerium/pomerium/internal/databroker"
"github.com/pomerium/pomerium/internal/events"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/registry"
"github.com/pomerium/pomerium/internal/version"
"github.com/pomerium/pomerium/pkg/envoy"
"github.com/pomerium/pomerium/pkg/envoy/files"
"github.com/pomerium/pomerium/proxy"
)
// Run runs the main pomerium application.
func Run(ctx context.Context, src config.Source) error {
log.Info(ctx).
Str("envoy_version", files.FullVersion()).
Str("version", version.FullVersion()).
Msg("cmd/pomerium")
src = databroker.NewConfigSource(ctx, src)
logMgr := config.NewLogManager(ctx, src)
defer logMgr.Close()
// trigger changes when underlying files are changed
src = config.NewFileWatcherSource(src)
src, err := autocert.New(src)
if err != nil {
return err
}
// override the default http transport so we can use the custom CA in the TLS client config (#1570)
http.DefaultTransport = config.NewHTTPTransport(src)
metricsMgr := config.NewMetricsManager(ctx, src)
defer metricsMgr.Close()
traceMgr := config.NewTraceManager(ctx, src)
defer traceMgr.Close()
eventsMgr := events.New()
// setup the control plane
controlPlane, err := controlplane.NewServer(src.GetConfig(), metricsMgr, eventsMgr)
if err != nil {
return fmt.Errorf("error creating control plane: %w", err)
}
src.OnConfigChange(ctx,
func(ctx context.Context, cfg *config.Config) {
if err := controlPlane.OnConfigChange(ctx, cfg); err != nil {
log.Error(ctx).Err(err).Msg("config change")
}
})
if err = controlPlane.OnConfigChange(ctx, src.GetConfig()); err != nil {
return fmt.Errorf("applying config: %w", err)
}
log.Info(ctx).
Str("grpc-port", src.GetConfig().GRPCPort).
Str("http-port", src.GetConfig().HTTPPort).
Str("outbound-port", src.GetConfig().OutboundPort).
Str("metrics-port", src.GetConfig().MetricsPort).
Str("debug-port", src.GetConfig().DebugPort).
Msg("server started")
// create envoy server
envoyServer, err := envoy.NewServer(ctx, src, controlPlane.Builder)
if err != nil {
return fmt.Errorf("error creating envoy server: %w", err)
}
defer envoyServer.Close()
// add services
if err := setupAuthenticate(ctx, src, controlPlane); err != nil {
return err
}
var authorizeServer *authorize.Authorize
if config.IsAuthorize(src.GetConfig().Options.Services) {
authorizeServer, err = setupAuthorize(ctx, src, controlPlane)
if err != nil {
return err
}
}
var dataBrokerServer *databroker_service.DataBroker
if config.IsDataBroker(src.GetConfig().Options.Services) {
dataBrokerServer, err = setupDataBroker(ctx, src, controlPlane, eventsMgr)
if err != nil {
return fmt.Errorf("setting up databroker: %w", err)
}
}
if err = setupRegistryReporter(ctx, src); err != nil {
return fmt.Errorf("setting up registry reporter: %w", err)
}
if err := setupProxy(ctx, src, controlPlane); err != nil {
return err
}
ctx, cancel := context.WithCancel(ctx)
go func(ctx context.Context) {
ch := make(chan os.Signal, 2)
defer signal.Stop(ch)
signal.Notify(ch, os.Interrupt)
signal.Notify(ch, syscall.SIGTERM)
select {
case <-ch:
case <-ctx.Done():
}
cancel()
}(ctx)
// run everything
eg, ctx := errgroup.WithContext(ctx)
if authorizeServer != nil {
eg.Go(func() error {
return authorizeServer.Run(ctx)
})
}
eg.Go(func() error {
return controlPlane.Run(ctx)
})
if dataBrokerServer != nil {
eg.Go(func() error {
return dataBrokerServer.Run(ctx)
})
}
return eg.Wait()
}
func setupAuthenticate(ctx context.Context, src config.Source, controlPlane *controlplane.Server) error {
if !config.IsAuthenticate(src.GetConfig().Options.Services) {
return nil
}
svc, err := authenticate.New(src.GetConfig())
if err != nil {
return fmt.Errorf("error creating authenticate service: %w", err)
}
err = controlPlane.EnableAuthenticate(svc)
if err != nil {
return fmt.Errorf("error adding authenticate service to control plane: %w", err)
}
src.OnConfigChange(ctx, svc.OnConfigChange)
svc.OnConfigChange(ctx, src.GetConfig())
log.Info(ctx).Msg("enabled authenticate service")
return nil
}
func setupAuthorize(ctx context.Context, src config.Source, controlPlane *controlplane.Server) (*authorize.Authorize, error) {
svc, err := authorize.New(src.GetConfig())
if err != nil {
return nil, fmt.Errorf("error creating authorize service: %w", err)
}
envoy_service_auth_v3.RegisterAuthorizationServer(controlPlane.GRPCServer, svc)
log.Info(context.TODO()).Msg("enabled authorize service")
src.OnConfigChange(ctx, svc.OnConfigChange)
svc.OnConfigChange(ctx, src.GetConfig())
return svc, nil
}
func setupDataBroker(ctx context.Context,
src config.Source,
controlPlane *controlplane.Server,
eventsMgr *events.Manager,
) (*databroker_service.DataBroker, error) {
svc, err := databroker_service.New(src.GetConfig(), eventsMgr)
if err != nil {
return nil, fmt.Errorf("error creating databroker service: %w", err)
}
svc.Register(controlPlane.GRPCServer)
log.Info(context.TODO()).Msg("enabled databroker service")
src.OnConfigChange(ctx, svc.OnConfigChange)
svc.OnConfigChange(ctx, src.GetConfig())
return svc, nil
}
func setupRegistryReporter(ctx context.Context, src config.Source) error {
reporter := registry.NewReporter()
src.OnConfigChange(ctx, reporter.OnConfigChange)
reporter.OnConfigChange(ctx, src.GetConfig())
return nil
}
func setupProxy(ctx context.Context, src config.Source, controlPlane *controlplane.Server) error {
if !config.IsProxy(src.GetConfig().Options.Services) {
return nil
}
svc, err := proxy.New(src.GetConfig())
if err != nil {
return fmt.Errorf("error creating proxy service: %w", err)
}
err = controlPlane.EnableProxy(svc)
if err != nil {
return fmt.Errorf("error adding proxy service to control plane: %w", err)
}
log.Info(context.TODO()).Msg("enabled proxy service")
src.OnConfigChange(ctx, svc.OnConfigChange)
svc.OnConfigChange(ctx, src.GetConfig())
return nil
}

View file

@ -0,0 +1,148 @@
package pomerium_test
import (
"context"
"os"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/pomerium/pomerium/config"
"github.com/pomerium/pomerium/pkg/cmd/pomerium"
"github.com/pomerium/pomerium/pkg/envoy/files"
)
func Test_run(t *testing.T) {
os.Clearenv()
run := func(ctx context.Context, configFile string) error {
src, err := config.NewFileOrEnvironmentSource(configFile, files.FullVersion())
if err != nil {
return err
}
return pomerium.Run(ctx, src)
}
tests := []struct {
name string
configFileFlag string
check func(require.TestingT, error, ...any)
}{
{"nil configuration", "", require.Error},
{"bad proxy no authenticate url", `
{
"address": ":9433",
"grpc_address": ":9444",
"insecure_server": true,
"authorize_service_url": "https://authorize.corp.example",
"shared_secret": "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=",
"cookie_secret": "zixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=",
"services": "proxy",
"policy": [{ "from": "https://pomerium.io", "to": "https://httpbin.org" }]
}
`, require.Error},
{"bad authenticate no cookie secret", `
{
"address": ":9433",
"grpc_address": ":9444",
"insecure_server": true,
"authenticate_service_url": "https://authenticate.corp.example",
"shared_secret": "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=",
"services": "authenticate",
"policy": [{ "from": "https://pomerium.io", "to": "https://httpbin.org" }]
}
`, require.Error},
{"bad authorize service bad shared key", `
{
"address": ":9433",
"grpc_address": ":9444",
"insecure_server": true,
"authorize_service_url": "https://authorize.corp.example",
"shared_secret": "^^^",
"cookie_secret": "zixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=",
"services": "authorize",
"policy": [{ "from": "https://pomerium.io", "to": "https://httpbin.org" }]
}
`, require.Error},
{"bad http port", `
{
"address": ":-1",
"grpc_address": ":9444",
"grpc_insecure": true,
"insecure_server": true,
"authorize_service_url": "https://authorize.corp.example",
"authenticate_service_url": "https://authenticate.corp.example",
"shared_secret": "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=",
"cookie_secret": "zixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=",
"services": "proxy",
"policy": [{ "from": "https://pomerium.io", "to": "https://httpbin.org" }]
}
`, require.Error},
{"bad redirect port", `
{
"address": ":9433",
"http_redirect_addr":":-1",
"grpc_address": ":9444",
"grpc_insecure": true,
"insecure_server": true,
"authorize_service_url": "https://authorize.corp.example",
"authenticate_service_url": "https://authenticate.corp.example",
"shared_secret": "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=",
"cookie_secret": "zixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=",
"services": "proxy",
"policy": [{ "from": "https://pomerium.io", "to": "https://httpbin.org" }]
}
`, require.Error},
{"bad metrics port ", `
{
"address": ":9433",
"metrics_address": ":-1",
"grpc_insecure": true,
"insecure_server": true,
"authorize_service_url": "https://authorize.corp.example",
"authenticate_service_url": "https://authenticate.corp.example",
"shared_secret": "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=",
"cookie_secret": "zixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=",
"services": "proxy",
"policy": [{ "from": "https://pomerium.io", "to": "https://httpbin.org" }]
}
`, require.Error},
{"malformed tracing provider", `
{
"tracing_provider": "bad tracing provider",
"address": ":9433",
"grpc_address": ":9444",
"grpc_insecure": true,
"insecure_server": true,
"authorize_service_url": "https://authorize.corp.example",
"authenticate_service_url": "https://authenticate.corp.example",
"shared_secret": "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=",
"cookie_secret": "zixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=",
"services": "proxy",
"policy": [{ "from": "https://pomerium.io", "to": "https://httpbin.org" }]
}
`, require.Error},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpFile, err := os.CreateTemp(os.TempDir(), "*.json")
if err != nil {
t.Fatal("Cannot create temporary file", err)
}
defer func() { _ = os.Remove(tmpFile.Name()) }()
fn := tmpFile.Name()
if _, err := tmpFile.Write([]byte(tt.configFileFlag)); err != nil {
tmpFile.Close()
t.Fatal(err)
}
configFile := fn
ctx, clearTimeout := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer clearTimeout()
tt.check(t, run(ctx, configFile))
})
}
}

355
pkg/envoy/envoy.go Normal file
View file

@ -0,0 +1,355 @@
// Package envoy creates and configures an envoy server.
package envoy
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"time"
"github.com/cenkalti/backoff/v4"
envoy_config_bootstrap_v3 "github.com/envoyproxy/go-control-plane/envoy/config/bootstrap/v3"
envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
"github.com/google/go-cmp/cmp"
"github.com/natefinch/atomic"
"github.com/rs/zerolog"
"github.com/shirou/gopsutil/v3/process"
"google.golang.org/protobuf/encoding/protojson"
"github.com/pomerium/pomerium/config"
"github.com/pomerium/pomerium/config/envoyconfig"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/telemetry"
"github.com/pomerium/pomerium/pkg/envoy/files"
)
const (
configFileName = "envoy-config.yaml"
)
type serverOptions struct {
services string
logLevel string
}
// A Server is a pomerium proxy implemented via envoy.
type Server struct {
wd string
cmd *exec.Cmd
builder *envoyconfig.Builder
grpcPort, httpPort string
envoyPath string
monitorProcessCancel context.CancelFunc
mu sync.Mutex
options serverOptions
}
// NewServer creates a new server with traffic routed by envoy.
func NewServer(ctx context.Context, src config.Source, builder *envoyconfig.Builder) (*Server, error) {
envoyPath, err := Extract()
if err != nil {
return nil, fmt.Errorf("extracting envoy: %w", err)
}
srv := &Server{
wd: path.Dir(envoyPath),
builder: builder,
grpcPort: src.GetConfig().GRPCPort,
httpPort: src.GetConfig().HTTPPort,
envoyPath: envoyPath,
monitorProcessCancel: func() {},
}
go srv.runProcessCollector(ctx)
src.OnConfigChange(ctx, srv.onConfigChange)
srv.onConfigChange(ctx, src.GetConfig())
log.Info(ctx).
Str("path", envoyPath).
Str("checksum", files.Checksum()).
Msg("running envoy")
return srv, nil
}
// Close kills any underlying envoy process.
func (srv *Server) Close() error {
srv.monitorProcessCancel()
srv.mu.Lock()
defer srv.mu.Unlock()
var err error
if srv.cmd != nil && srv.cmd.Process != nil {
err = srv.cmd.Process.Kill()
if err != nil {
log.Error(context.TODO()).Err(err).Str("service", "envoy").Msg("envoy: failed to kill process on close")
}
srv.cmd = nil
}
return err
}
func (srv *Server) onConfigChange(ctx context.Context, cfg *config.Config) {
srv.update(ctx, cfg)
}
func (srv *Server) update(ctx context.Context, cfg *config.Config) {
srv.mu.Lock()
defer srv.mu.Unlock()
options := serverOptions{
services: cfg.Options.Services,
logLevel: firstNonEmpty(cfg.Options.ProxyLogLevel, cfg.Options.LogLevel, "debug"),
}
if cmp.Equal(srv.options, options, cmp.AllowUnexported(serverOptions{})) {
log.Debug(ctx).Str("service", "envoy").Msg("envoy: no config changes detected")
return
}
srv.options = options
log.Info(ctx).Msg("envoy: starting envoy process")
if err := srv.run(ctx, cfg); err != nil {
log.Error(ctx).Err(err).Str("service", "envoy").Msg("envoy: failed to run envoy process")
return
}
}
func (srv *Server) run(ctx context.Context, cfg *config.Config) error {
// cancel any process monitor since we will be killing the previous process
srv.monitorProcessCancel()
if err := srv.writeConfig(ctx, cfg); err != nil {
log.Error(ctx).Err(err).Str("service", "envoy").Msg("envoy: failed to write envoy config")
return err
}
args := []string{
"-c", configFileName,
"--log-level", srv.options.logLevel,
"--log-format", "[LOG_FORMAT]%l--%n--%v",
"--log-format-escaped",
}
exePath, args := srv.prepareRunEnvoyCommand(ctx, args)
cmd := exec.Command(exePath, args...)
cmd.Dir = srv.wd
stderr, err := cmd.StderrPipe()
if err != nil {
return fmt.Errorf("error creating stderr pipe for envoy: %w", err)
}
go srv.handleLogs(ctx, stderr)
stdout, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("error creating stdout pipe for envoy: %w", err)
}
go srv.handleLogs(ctx, stdout)
// make sure envoy is killed if we're killed
cmd.SysProcAttr = sysProcAttr
err = cmd.Start()
if err != nil {
return fmt.Errorf("error starting envoy: %w", err)
}
// call Wait to avoid zombie processes
go func() { _ = cmd.Wait() }()
// monitor the process so we exit if it prematurely exits
var monitorProcessCtx context.Context
monitorProcessCtx, srv.monitorProcessCancel = context.WithCancel(context.Background())
go srv.monitorProcess(monitorProcessCtx, int32(cmd.Process.Pid))
srv.cmd = cmd
return nil
}
func (srv *Server) writeConfig(ctx context.Context, cfg *config.Config) error {
confBytes, err := srv.buildBootstrapConfig(cfg)
if err != nil {
return err
}
cfgPath := filepath.Join(srv.wd, configFileName)
log.Debug(ctx).Str("service", "envoy").Str("location", cfgPath).Msg("wrote config file to location")
return atomic.WriteFile(cfgPath, bytes.NewReader(confBytes))
}
func (srv *Server) buildBootstrapConfig(cfg *config.Config) ([]byte, error) {
nodeCfg := &envoy_config_core_v3.Node{
Id: telemetry.ServiceName(cfg.Options.Services),
Cluster: telemetry.ServiceName(cfg.Options.Services),
}
adminCfg, err := srv.builder.BuildBootstrapAdmin(cfg)
if err != nil {
return nil, err
}
dynamicCfg := &envoy_config_bootstrap_v3.Bootstrap_DynamicResources{
AdsConfig: &envoy_config_core_v3.ApiConfigSource{
ApiType: envoy_config_core_v3.ApiConfigSource_ApiType(envoy_config_core_v3.ApiConfigSource_ApiType_value["DELTA_GRPC"]),
TransportApiVersion: envoy_config_core_v3.ApiVersion_V3,
GrpcServices: []*envoy_config_core_v3.GrpcService{
{
TargetSpecifier: &envoy_config_core_v3.GrpcService_EnvoyGrpc_{
EnvoyGrpc: &envoy_config_core_v3.GrpcService_EnvoyGrpc{
ClusterName: "pomerium-control-plane-grpc",
},
},
},
},
},
LdsConfig: &envoy_config_core_v3.ConfigSource{
ResourceApiVersion: envoy_config_core_v3.ApiVersion_V3,
ConfigSourceSpecifier: &envoy_config_core_v3.ConfigSource_Ads{},
},
CdsConfig: &envoy_config_core_v3.ConfigSource{
ResourceApiVersion: envoy_config_core_v3.ApiVersion_V3,
ConfigSourceSpecifier: &envoy_config_core_v3.ConfigSource_Ads{},
},
}
staticCfg, err := srv.builder.BuildBootstrapStaticResources()
if err != nil {
return nil, err
}
statsCfg, err := srv.builder.BuildBootstrapStatsConfig(cfg)
if err != nil {
return nil, err
}
layeredRuntimeCfg, err := srv.builder.BuildBootstrapLayeredRuntime()
if err != nil {
return nil, err
}
bootstrapCfg := &envoy_config_bootstrap_v3.Bootstrap{
Node: nodeCfg,
Admin: adminCfg,
DynamicResources: dynamicCfg,
StaticResources: staticCfg,
StatsConfig: statsCfg,
LayeredRuntime: layeredRuntimeCfg,
}
jsonBytes, err := protojson.Marshal(bootstrapCfg)
if err != nil {
return nil, err
}
return jsonBytes, nil
}
var fileNameAndNumberRE = regexp.MustCompile(`^(\[[a-zA-Z0-9/-_.]+:[0-9]+])\s(.*)$`)
func (srv *Server) parseLog(line string) (name string, logLevel string, msg string) {
// format: [LOG_FORMAT]level--name--message
// message is c-escaped
parts := strings.SplitN(line, "--", 3)
if len(parts) == 3 {
logLevel = strings.TrimPrefix(parts[0], "[LOG_FORMAT]")
name = parts[1]
msg = parts[2]
}
return
}
func (srv *Server) handleLogs(ctx context.Context, rc io.ReadCloser) {
defer rc.Close()
l := log.With().Str("service", "envoy").Logger()
bo := backoff.NewExponentialBackOff()
s := bufio.NewReader(rc)
for {
ln, err := s.ReadString('\n')
if err != nil {
if errors.Is(err, io.EOF) || errors.Is(err, os.ErrClosed) {
break
}
log.Error(ctx).Err(err).Msg("failed to read log")
time.Sleep(bo.NextBackOff())
continue
}
ln = strings.TrimRight(ln, "\r\n")
bo.Reset()
name, logLevel, msg := srv.parseLog(ln)
if name == "" {
name = "envoy"
}
lvl := zerolog.ErrorLevel
if x, err := zerolog.ParseLevel(logLevel); err == nil {
lvl = x
}
if msg == "" {
msg = ln
}
msg = fileNameAndNumberRE.ReplaceAllString(msg, "\"$2\"")
if s, err := strconv.Unquote(msg); err == nil {
msg = s
}
// ignore empty messages
if msg == "" {
continue
}
l.WithLevel(lvl).
Str("name", name).
Msg(msg)
}
}
func (srv *Server) monitorProcess(ctx context.Context, pid int32) {
log.Info(ctx).
Int32("pid", pid).
Msg("envoy: start monitoring subprocess")
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
exists, err := process.PidExistsWithContext(ctx, pid)
if err != nil {
log.Fatal().Err(err).
Int32("pid", pid).
Msg("envoy: error retrieving subprocess information")
} else if !exists {
log.Fatal().Err(err).
Int32("pid", pid).
Msg("envoy: subprocess exited")
}
// wait for the next tick
select {
case <-ctx.Done():
return
case <-ticker.C:
}
}
}

29
pkg/envoy/envoy_darwin.go Normal file
View file

@ -0,0 +1,29 @@
//go:build darwin
// +build darwin
package envoy
import (
"context"
"syscall"
"github.com/pomerium/pomerium/internal/log"
)
var sysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
}
func (srv *Server) runProcessCollector(ctx context.Context) {}
func (srv *Server) prepareRunEnvoyCommand(ctx context.Context, sharedArgs []string) (exePath string, args []string) {
if srv.cmd != nil && srv.cmd.Process != nil {
log.Info(ctx).Msg("envoy: terminating previous envoy process")
_ = srv.cmd.Process.Kill()
}
args = make([]string, len(sharedArgs))
copy(args, sharedArgs)
return srv.envoyPath, args
}

113
pkg/envoy/envoy_linux.go Normal file
View file

@ -0,0 +1,113 @@
//go:build linux
// +build linux
package envoy
import (
"context"
"os"
"strconv"
"sync"
"syscall"
"time"
"go.opencensus.io/stats/view"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/telemetry/metrics"
)
const baseIDPath = "/tmp/pomerium-envoy-base-id"
var restartEpoch struct {
sync.Mutex
value int
}
var sysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
Pdeathsig: syscall.SIGTERM,
}
func (srv *Server) runProcessCollector(ctx context.Context) {
pc := metrics.NewProcessCollector("envoy")
if err := view.Register(pc.Views()...); err != nil {
log.Error(ctx).Err(err).Msg("failed to register envoy process metric views")
}
defer view.Unregister(pc.Views()...)
const collectInterval = time.Second * 10
ticker := time.NewTicker(collectInterval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
}
var pid int
srv.mu.Lock()
if srv.cmd != nil && srv.cmd.Process != nil {
pid = srv.cmd.Process.Pid
}
srv.mu.Unlock()
if pid > 0 {
err := pc.Measure(ctx, pid)
if err != nil {
log.Error(ctx).Err(err).Msg("failed to measure envoy process metrics")
}
}
}
}
func (srv *Server) prepareRunEnvoyCommand(ctx context.Context, sharedArgs []string) (exePath string, args []string) {
// release the previous process so we can hot-reload
if srv.cmd != nil && srv.cmd.Process != nil {
log.Info(ctx).Msg("envoy: releasing envoy process for hot-reload")
err := srv.cmd.Process.Release()
if err != nil {
log.Warn(ctx).Err(err).Str("service", "envoy").Msg("envoy: failed to release envoy process for hot-reload")
}
}
args = make([]string, len(sharedArgs))
copy(args, sharedArgs)
restartEpoch.Lock()
if baseID, ok := readBaseID(); ok {
args = append(args,
"--base-id", strconv.Itoa(baseID),
"--restart-epoch", strconv.Itoa(restartEpoch.value),
"--drain-time-s", "60",
"--parent-shutdown-time-s", "120",
"--drain-strategy", "immediate",
)
restartEpoch.value++
} else {
args = append(args,
"--use-dynamic-base-id",
"--base-id-path", baseIDPath,
)
restartEpoch.value = 1
}
restartEpoch.Unlock()
return srv.envoyPath, args
}
func readBaseID() (int, bool) {
bs, err := os.ReadFile(baseIDPath)
if err != nil {
return 0, false
}
baseID, err := strconv.Atoi(string(bs))
if err != nil {
return 0, false
}
return baseID, true
}

24
pkg/envoy/envoy_other.go Normal file
View file

@ -0,0 +1,24 @@
//go:build !linux && !darwin
// +build !linux,!darwin
package envoy
import (
"context"
"github.com/pomerium/pomerium/internal/log"
)
func (srv *Server) runProcessCollector(ctx context.Context) {}
func (srv *Server) prepareRunEnvoyCommand(ctx context.Context, sharedArgs []string) (exePath string, args []string) {
if srv.cmd != nil && srv.cmd.Process != nil {
log.Info(ctx).Msg("envoy: terminating previous envoy process")
_ = srv.cmd.Process.Kill()
}
args = make([]string, len(sharedArgs))
copy(args, sharedArgs)
return srv.envoyPath, args
}

49
pkg/envoy/envoy_test.go Normal file
View file

@ -0,0 +1,49 @@
package envoy
import (
"context"
"io"
"regexp"
"strings"
"testing"
"github.com/rs/zerolog"
)
func TestServer_handleLogs(t *testing.T) {
logFormatRE := regexp.MustCompile(`^[[]LOG_FORMAT[]](.*?)--(.*?)--(.*?)$`)
line := "[LOG_FORMAT]debug--filter--[external/envoy/source/extensions/filters/listener/tls_inspector/tls_inspector.cc:78] tls inspector: new connection accepted"
old := func(s string) string {
msg := s
parts := logFormatRE.FindStringSubmatch(s)
if len(parts) == 4 {
msg = parts[3]
}
return msg
}
srv := &Server{}
expectedMsg := old(line)
name, logLevel, gotMsg := srv.parseLog(line)
if name != "filter" {
t.Errorf("unexpected name, want filter, got: %s", name)
}
if logLevel != "debug" {
t.Errorf("unexpected log level, want debug, got: %s", logLevel)
}
if gotMsg != expectedMsg {
t.Errorf("unexpected msg, want %s, got: %s", expectedMsg, gotMsg)
}
}
func Benchmark_handleLogs(b *testing.B) {
line := `[LOG_FORMAT]debug--http--[external/envoy/source/common/http/conn_manager_impl.cc:781] [C25][S14758077654018620250] request headers complete (end_stream=false):\\n\\':authority\\', \\'enabled-ws-echo.localhost.pomerium.io\\'\\n\\':path\\', \\'/\\'\\n\\':method\\', \\'GET\\'\\n\\'upgrade\\', \\'websocket\\'\\n\\'connection\\', \\'upgrade\\'\\n\\'x-request-id\\', \\'30ac7726e0b9e00a9c9ab2bf66d692ac\\'\\n\\'x-real-ip\\', \\'172.17.0.1\\'\\n\\'x-forwarded-for\\', \\'172.17.0.1\\'\\n\\'x-forwarded-host\\', \\'enabled-ws-echo.localhost.pomerium.io\\'\\n\\'x-forwarded-port\\', \\'443\\'\\n\\'x-forwarded-proto\\', \\'https\\'\\n\\'x-scheme\\', \\'https\\'\\n\\'user-agent\\', \\'Go-http-client/1.1\\'\\n\\'sec-websocket-key\\', \\'4bh7+YFVzrJiblaSu/CVfg==\\'\\n\\'sec-websocket-version\\', \\'13\\'`
rc := io.NopCloser(strings.NewReader(line))
srv := &Server{}
zerolog.SetGlobalLevel(zerolog.InfoLevel)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
srv.handleLogs(context.Background(), rc)
}
}

99
pkg/envoy/extract.go Normal file
View file

@ -0,0 +1,99 @@
package envoy
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"hash"
"io"
"os"
"path/filepath"
"strings"
"sync"
"github.com/pomerium/pomerium/pkg/envoy/files"
)
const (
ownerRX = os.FileMode(0o500)
maxExpandedEnvoySize = 1 << 30
)
type hashReader struct {
hash.Hash
r io.Reader
}
func (hr *hashReader) Read(p []byte) (n int, err error) {
n, err = hr.r.Read(p)
_, _ = hr.Write(p[:n])
return n, err
}
var (
setupLock sync.Mutex
setupDone bool
setupFullEnvoyPath string
setupErr error
)
// Extract extracts envoy binary and returns its location
func Extract() (fullEnvoyPath string, err error) {
setupLock.Lock()
defer setupLock.Unlock()
// if we've extract at least once, and the file we previously extracted no longer exists, force a new extraction
if setupFullEnvoyPath != "" {
if _, err := os.Stat(setupFullEnvoyPath); os.IsNotExist(err) {
setupDone = false
}
}
if setupDone {
return setupFullEnvoyPath, setupErr
}
dir, err := os.MkdirTemp(os.TempDir(), "pomerium-envoy")
if err != nil {
setupErr = fmt.Errorf("envoy: failed making temporary working dir: %w", err)
return
}
setupFullEnvoyPath = filepath.Join(dir, "envoy")
err = extract(setupFullEnvoyPath)
if err != nil {
setupErr = fmt.Errorf("envoy: failed to extract embedded envoy binary: %w", err)
return
}
setupDone = true
return setupFullEnvoyPath, setupErr
}
func extract(dstName string) (err error) {
checksum, err := hex.DecodeString(strings.Fields(files.Checksum())[0])
if err != nil {
return fmt.Errorf("checksum %s: %w", files.Checksum(), err)
}
hr := &hashReader{
Hash: sha256.New(),
r: bytes.NewReader(files.Binary()),
}
dst, err := os.OpenFile(dstName, os.O_CREATE|os.O_WRONLY, ownerRX)
if err != nil {
return err
}
defer func() { err = dst.Close() }()
if _, err = io.Copy(dst, io.LimitReader(hr, maxExpandedEnvoySize)); err != nil {
return err
}
sum := hr.Sum(nil)
if !bytes.Equal(sum, checksum) {
return fmt.Errorf("expected %x, got %x checksum", checksum, sum)
}
return nil
}

27
pkg/envoy/files/files.go Normal file
View file

@ -0,0 +1,27 @@
// Package files contains files for use with envoy.
package files
import (
_ "embed" // for embedded files
"strings"
)
// Binary returns the raw envoy binary bytes.
func Binary() []byte {
return rawBinary
}
// Checksum returns the checksum for the embedded envoy binary.
func Checksum() string {
return strings.Fields(rawChecksum)[0]
}
// FullVersion returns the full version string for envoy.
func FullVersion() string {
return Version() + "+" + Checksum()
}
// Version returns the envoy version.
func Version() string {
return strings.TrimSpace(rawVersion)
}

View file

@ -0,0 +1,14 @@
//go:build darwin && amd64 && !embed_pomerium
package files
import _ "embed" // embed
//go:embed envoy-darwin-amd64
var rawBinary []byte
//go:embed envoy-darwin-amd64.sha256
var rawChecksum string
//go:embed envoy-darwin-amd64.version
var rawVersion string

View file

@ -0,0 +1,15 @@
//go:build darwin && arm64 && !embed_pomerium
// +build darwin,arm64,!embed_pomerium
package files
import _ "embed" // embed
//go:embed envoy-darwin-arm64
var rawBinary []byte
//go:embed envoy-darwin-arm64.sha256
var rawChecksum string
//go:embed envoy-darwin-arm64.version
var rawVersion string

View file

@ -0,0 +1,16 @@
//go:build embed_pomerium
package files
var rawBinary []byte
var rawChecksum string
var rawVersion string
// SetFiles sets external source for envoy
func SetFiles(bin []byte, checksum, version string) {
rawBinary = bin
rawChecksum = checksum
rawVersion = version
}

View file

@ -0,0 +1,15 @@
//go:build linux && amd64 && !embed_pomerium
// +build linux,amd64,!embed_pomerium
package files
import _ "embed" // embed
//go:embed envoy-linux-amd64
var rawBinary []byte
//go:embed envoy-linux-amd64.sha256
var rawChecksum string
//go:embed envoy-linux-amd64.version
var rawVersion string

View file

@ -0,0 +1,15 @@
//go:build linux && arm64 && !embed_pomerium
// +build linux,arm64,!embed_pomerium
package files
import _ "embed" // embed
//go:embed envoy-linux-arm64
var rawBinary []byte
//go:embed envoy-linux-arm64.sha256
var rawChecksum string
//go:embed envoy-linux-arm64.version
var rawVersion string

37
pkg/envoy/misc.go Normal file
View file

@ -0,0 +1,37 @@
package envoy
import (
"fmt"
"net"
"strconv"
envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
)
func firstNonEmpty(args ...string) string {
for _, a := range args {
if a != "" {
return a
}
}
return ""
}
// ParseAddress parses a string address into an envoy address.
func ParseAddress(raw string) (*envoy_config_core_v3.Address, error) {
if host, portstr, err := net.SplitHostPort(raw); err == nil {
if port, err := strconv.Atoi(portstr); err == nil {
return &envoy_config_core_v3.Address{
Address: &envoy_config_core_v3.Address_SocketAddress{
SocketAddress: &envoy_config_core_v3.SocketAddress{
Address: host,
PortSpecifier: &envoy_config_core_v3.SocketAddress_PortValue{
PortValue: uint32(port),
},
},
},
}, nil
}
}
return nil, fmt.Errorf("unknown address format: %s", raw)
}