mirror of
https://github.com/pomerium/pomerium.git
synced 2025-06-03 03:12:50 +02:00
envoy: Initial changes
This commit is contained in:
parent
8f78497e99
commit
99e788a9b4
107 changed files with 2542 additions and 3322 deletions
39
internal/controlplane/grpc_accesslog.go
Normal file
39
internal/controlplane/grpc_accesslog.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package controlplane
|
||||
|
||||
import (
|
||||
envoy_service_accesslog_v2 "github.com/envoyproxy/go-control-plane/envoy/service/accesslog/v2"
|
||||
"github.com/pomerium/pomerium/internal/log"
|
||||
)
|
||||
|
||||
func (srv *Server) registerAccessLogHandlers() {
|
||||
envoy_service_accesslog_v2.RegisterAccessLogServiceServer(srv.GRPCServer, srv)
|
||||
}
|
||||
|
||||
// StreamAccessLogs receives logs from envoy and prints them to stdout.
|
||||
func (srv *Server) StreamAccessLogs(stream envoy_service_accesslog_v2.AccessLogService_StreamAccessLogsServer) error {
|
||||
for {
|
||||
msg, err := stream.Recv()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("access log stream error, disconnecting")
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range msg.GetHttpLogs().LogEntry {
|
||||
evt := log.Info().Str("service", "envoy")
|
||||
// common properties
|
||||
evt = evt.Str("upstream-cluster", entry.GetCommonProperties().GetUpstreamCluster())
|
||||
// request properties
|
||||
evt = evt.Str("method", entry.GetRequest().GetRequestMethod().String())
|
||||
evt = evt.Str("authority", entry.GetRequest().GetAuthority())
|
||||
evt = evt.Str("path", entry.GetRequest().GetPath())
|
||||
evt = evt.Str("user-agent", entry.GetRequest().GetUserAgent())
|
||||
evt = evt.Str("referer", entry.GetRequest().GetReferer())
|
||||
evt = evt.Str("forwarded-for", entry.GetRequest().GetForwardedFor())
|
||||
evt = evt.Str("request-id", entry.GetRequest().GetRequestId())
|
||||
// response properties
|
||||
evt = evt.Uint32("response-code", entry.GetResponse().GetResponseCode().GetValue())
|
||||
evt = evt.Str("response-code-details", entry.GetResponse().GetResponseCodeDetails())
|
||||
evt.Msg("http-request")
|
||||
}
|
||||
}
|
||||
}
|
135
internal/controlplane/grpc_xds.go
Normal file
135
internal/controlplane/grpc_xds.go
Normal file
|
@ -0,0 +1,135 @@
|
|||
package controlplane
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
envoy_service_discovery_v3 "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
|
||||
"github.com/pomerium/pomerium/internal/log"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
func (srv *Server) registerXDSHandlers() {
|
||||
envoy_service_discovery_v3.RegisterAggregatedDiscoveryServiceServer(srv.GRPCServer, srv)
|
||||
}
|
||||
|
||||
// StreamAggregatedResources streams xDS resources based on incoming discovery requests.
|
||||
//
|
||||
// This is setup as 3 concurrent goroutines:
|
||||
// - The first retrieves the requests from the client.
|
||||
// - The third sends responses back to the client.
|
||||
// - The second waits for either the client to request a new resource type
|
||||
// or for the config to have been updated
|
||||
// - in either case, we loop over all of the current client versions
|
||||
// and if any of them are different from the current version, we send
|
||||
// the updated resource
|
||||
func (srv *Server) StreamAggregatedResources(stream envoy_service_discovery_v3.AggregatedDiscoveryService_StreamAggregatedResourcesServer) error {
|
||||
incoming := make(chan *envoy_service_discovery_v3.DiscoveryRequest)
|
||||
outgoing := make(chan *envoy_service_discovery_v3.DiscoveryResponse)
|
||||
|
||||
eg, ctx := errgroup.WithContext(stream.Context())
|
||||
// receive requests
|
||||
eg.Go(func() error {
|
||||
return srv.streamAggregatedResourcesIncomingStep(ctx, stream, incoming)
|
||||
})
|
||||
eg.Go(func() error {
|
||||
return srv.streamAggregatedResourcesProcessStep(ctx, incoming, outgoing)
|
||||
})
|
||||
// send responses
|
||||
eg.Go(func() error {
|
||||
return srv.streamAggregatedResourcesOutgoingStep(ctx, stream, outgoing)
|
||||
})
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
func (srv *Server) streamAggregatedResourcesIncomingStep(
|
||||
ctx context.Context,
|
||||
stream envoy_service_discovery_v3.AggregatedDiscoveryService_StreamAggregatedResourcesServer,
|
||||
incoming chan<- *envoy_service_discovery_v3.DiscoveryRequest,
|
||||
) error {
|
||||
for {
|
||||
req, err := stream.Recv()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
select {
|
||||
case incoming <- req:
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (srv *Server) streamAggregatedResourcesProcessStep(
|
||||
ctx context.Context,
|
||||
incoming <-chan *envoy_service_discovery_v3.DiscoveryRequest,
|
||||
outgoing chan<- *envoy_service_discovery_v3.DiscoveryResponse,
|
||||
) error {
|
||||
versions := map[string]string{}
|
||||
|
||||
for {
|
||||
select {
|
||||
case req := <-incoming:
|
||||
if req.ErrorDetail != nil {
|
||||
bs, _ := json.Marshal(req.ErrorDetail.Details)
|
||||
log.Error().
|
||||
Err(errors.New(req.ErrorDetail.Message)).
|
||||
Int32("code", req.ErrorDetail.Code).
|
||||
RawJSON("details", bs).Msg("error applying configuration")
|
||||
continue
|
||||
}
|
||||
|
||||
// update the currently stored version
|
||||
// if this version is different from the current version
|
||||
// we will send the response below
|
||||
versions[req.TypeUrl] = req.VersionInfo
|
||||
case <-srv.configUpdated:
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
current := srv.currentConfig.Load().(versionedOptions)
|
||||
for typeURL, version := range versions {
|
||||
// the versions are different, so the envoy config needs to be updated
|
||||
if version != fmt.Sprint(current.version) {
|
||||
res, err := srv.buildDiscoveryResponse(fmt.Sprint(current.version), typeURL, current.Options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
select {
|
||||
case outgoing <- res:
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (srv *Server) streamAggregatedResourcesOutgoingStep(
|
||||
ctx context.Context,
|
||||
stream envoy_service_discovery_v3.AggregatedDiscoveryService_StreamAggregatedResourcesServer,
|
||||
outgoing <-chan *envoy_service_discovery_v3.DiscoveryResponse,
|
||||
) error {
|
||||
for {
|
||||
var res *envoy_service_discovery_v3.DiscoveryResponse
|
||||
select {
|
||||
case res = <-outgoing:
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
err := stream.Send(res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeltaAggregatedResources is not implemented.
|
||||
func (srv *Server) DeltaAggregatedResources(in envoy_service_discovery_v3.AggregatedDiscoveryService_DeltaAggregatedResourcesServer) error {
|
||||
return fmt.Errorf("DeltaAggregatedResources not implemented")
|
||||
}
|
39
internal/controlplane/http.go
Normal file
39
internal/controlplane/http.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
// Package controlplane contains the HTTP and gRPC base servers and the xDS gRPC implementation for envoy.
|
||||
package controlplane
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/pomerium/pomerium/internal/frontend"
|
||||
"github.com/pomerium/pomerium/internal/httputil"
|
||||
"github.com/pomerium/pomerium/internal/log"
|
||||
"github.com/pomerium/pomerium/internal/middleware"
|
||||
"github.com/pomerium/pomerium/internal/version"
|
||||
)
|
||||
|
||||
func (srv *Server) addHTTPMiddleware() {
|
||||
root := srv.HTTPRouter
|
||||
root.Use(log.NewHandler(log.Logger))
|
||||
root.Use(log.AccessHandler(func(r *http.Request, status, size int, duration time.Duration) {
|
||||
log.FromRequest(r).Debug().
|
||||
Dur("duration", duration).
|
||||
Int("size", size).
|
||||
Int("status", status).
|
||||
Str("method", r.Method).
|
||||
Str("host", r.Host).
|
||||
Str("path", r.URL.String()).
|
||||
Msg("http-request")
|
||||
}))
|
||||
root.Use(handlers.RecoveryHandler())
|
||||
root.Use(log.HeadersHandler(httputil.HeadersXForwarded))
|
||||
root.Use(log.RemoteAddrHandler("ip"))
|
||||
root.Use(log.UserAgentHandler("user_agent"))
|
||||
root.Use(log.RefererHandler("referer"))
|
||||
root.Use(log.RequestIDHandler("req_id", "Request-Id"))
|
||||
root.Use(middleware.Healthcheck("/ping", version.UserAgent()))
|
||||
root.HandleFunc("/healthz", httputil.HealthCheck)
|
||||
root.HandleFunc("/ping", httputil.HealthCheck)
|
||||
root.PathPrefix("/.pomerium/assets/").Handler(http.StripPrefix("/.pomerium/assets/", frontend.MustAssetHandler()))
|
||||
}
|
139
internal/controlplane/server.go
Normal file
139
internal/controlplane/server.go
Normal file
|
@ -0,0 +1,139 @@
|
|||
package controlplane
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/reflection"
|
||||
|
||||
"github.com/pomerium/pomerium/config"
|
||||
"github.com/pomerium/pomerium/internal/log"
|
||||
)
|
||||
|
||||
type versionedOptions struct {
|
||||
config.Options
|
||||
version int64
|
||||
}
|
||||
|
||||
// A Server is the control-plane gRPC and HTTP servers.
|
||||
type Server struct {
|
||||
GRPCListener net.Listener
|
||||
GRPCServer *grpc.Server
|
||||
HTTPListener net.Listener
|
||||
HTTPRouter *mux.Router
|
||||
|
||||
currentConfig atomic.Value
|
||||
configUpdated chan struct{}
|
||||
}
|
||||
|
||||
// NewServer creates a new Server. Listener ports are chosen by the OS.
|
||||
func NewServer() (*Server, error) {
|
||||
srv := &Server{
|
||||
configUpdated: make(chan struct{}, 1),
|
||||
}
|
||||
srv.currentConfig.Store(versionedOptions{})
|
||||
|
||||
var err error
|
||||
|
||||
// setup gRPC
|
||||
srv.GRPCListener, err = net.Listen("tcp4", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
srv.GRPCServer = grpc.NewServer()
|
||||
reflection.Register(srv.GRPCServer)
|
||||
srv.registerXDSHandlers()
|
||||
srv.registerAccessLogHandlers()
|
||||
|
||||
// setup HTTP
|
||||
srv.HTTPListener, err = net.Listen("tcp4", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
_ = srv.GRPCListener.Close()
|
||||
return nil, err
|
||||
}
|
||||
srv.HTTPRouter = mux.NewRouter()
|
||||
srv.addHTTPMiddleware()
|
||||
|
||||
return srv, nil
|
||||
}
|
||||
|
||||
// Run runs the control-plane gRPC and HTTP servers.
|
||||
func (srv *Server) Run(ctx context.Context) error {
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
// start the gRPC server
|
||||
eg.Go(func() error {
|
||||
log.Info().Str("addr", srv.GRPCListener.Addr().String()).Msg("starting control-plane gRPC server")
|
||||
return srv.GRPCServer.Serve(srv.GRPCListener)
|
||||
})
|
||||
|
||||
// gracefully stop the gRPC server on context cancellation
|
||||
eg.Go(func() error {
|
||||
<-ctx.Done()
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
ctx, cleanup := context.WithTimeout(ctx, time.Second*5)
|
||||
defer cleanup()
|
||||
|
||||
go func() {
|
||||
srv.GRPCServer.GracefulStop()
|
||||
cancel()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
srv.GRPCServer.Stop()
|
||||
cancel()
|
||||
}()
|
||||
|
||||
<-ctx.Done()
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
hsrv := (&http.Server{
|
||||
BaseContext: func(li net.Listener) context.Context {
|
||||
return ctx
|
||||
},
|
||||
Handler: srv.HTTPRouter,
|
||||
})
|
||||
|
||||
// start the HTTP server
|
||||
eg.Go(func() error {
|
||||
log.Info().Str("addr", srv.HTTPListener.Addr().String()).Msg("starting control-plane HTTP server")
|
||||
return hsrv.Serve(srv.HTTPListener)
|
||||
})
|
||||
|
||||
// gracefully stop the HTTP server on context cancellation
|
||||
eg.Go(func() error {
|
||||
<-ctx.Done()
|
||||
|
||||
ctx, cleanup := context.WithTimeout(ctx, time.Second*5)
|
||||
defer cleanup()
|
||||
|
||||
return hsrv.Shutdown(ctx)
|
||||
})
|
||||
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
// UpdateOptions updates the pomerium config options.
|
||||
func (srv *Server) UpdateOptions(options config.Options) error {
|
||||
select {
|
||||
case <-srv.configUpdated:
|
||||
default:
|
||||
}
|
||||
prev := srv.currentConfig.Load().(versionedOptions)
|
||||
srv.currentConfig.Store(versionedOptions{
|
||||
Options: options,
|
||||
version: prev.version + 1,
|
||||
})
|
||||
srv.configUpdated <- struct{}{}
|
||||
return nil
|
||||
}
|
105
internal/controlplane/xds.go
Normal file
105
internal/controlplane/xds.go
Normal file
|
@ -0,0 +1,105 @@
|
|||
package controlplane
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/pomerium/pomerium/config"
|
||||
|
||||
envoy_config_accesslog_v3 "github.com/envoyproxy/go-control-plane/envoy/config/accesslog/v3"
|
||||
envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
||||
envoy_extensions_access_loggers_grpc_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/access_loggers/grpc/v3"
|
||||
envoy_service_discovery_v3 "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
"github.com/golang/protobuf/ptypes/any"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func (srv *Server) buildDiscoveryResponse(version string, typeURL string, options config.Options) (*envoy_service_discovery_v3.DiscoveryResponse, error) {
|
||||
switch typeURL {
|
||||
case "type.googleapis.com/envoy.config.listener.v3.Listener":
|
||||
listeners := srv.buildListeners(options)
|
||||
anys := make([]*any.Any, len(listeners))
|
||||
for i, listener := range listeners {
|
||||
a, err := ptypes.MarshalAny(listener)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "error marshaling type to any: %v", err)
|
||||
}
|
||||
anys[i] = a
|
||||
}
|
||||
return &envoy_service_discovery_v3.DiscoveryResponse{
|
||||
VersionInfo: version,
|
||||
Resources: anys,
|
||||
TypeUrl: typeURL,
|
||||
}, nil
|
||||
case "type.googleapis.com/envoy.config.cluster.v3.Cluster":
|
||||
clusters := srv.buildClusters(options)
|
||||
anys := make([]*any.Any, len(clusters))
|
||||
for i, cluster := range clusters {
|
||||
a, err := ptypes.MarshalAny(cluster)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "error marshaling type to any: %v", err)
|
||||
}
|
||||
anys[i] = a
|
||||
}
|
||||
return &envoy_service_discovery_v3.DiscoveryResponse{
|
||||
VersionInfo: version,
|
||||
Resources: anys,
|
||||
TypeUrl: typeURL,
|
||||
}, nil
|
||||
default:
|
||||
return nil, status.Errorf(codes.Internal, "received request for unknown discovery request type: %s", typeURL)
|
||||
}
|
||||
}
|
||||
|
||||
func (srv *Server) buildAccessLog() *envoy_config_accesslog_v3.AccessLog {
|
||||
tc, _ := ptypes.MarshalAny(&envoy_extensions_access_loggers_grpc_v3.HttpGrpcAccessLogConfig{
|
||||
CommonConfig: &envoy_extensions_access_loggers_grpc_v3.CommonGrpcAccessLogConfig{
|
||||
LogName: "ingress-http",
|
||||
GrpcService: &envoy_config_core_v3.GrpcService{
|
||||
TargetSpecifier: &envoy_config_core_v3.GrpcService_EnvoyGrpc_{
|
||||
EnvoyGrpc: &envoy_config_core_v3.GrpcService_EnvoyGrpc{
|
||||
ClusterName: "pomerium-control-plane-grpc",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
return &envoy_config_accesslog_v3.AccessLog{
|
||||
Name: "envoy.access_loggers.http_grpc",
|
||||
ConfigType: &envoy_config_accesslog_v3.AccessLog_TypedConfig{TypedConfig: tc},
|
||||
}
|
||||
}
|
||||
|
||||
func buildAddress(hostport string, defaultPort int) *envoy_config_core_v3.Address {
|
||||
host, strport, err := net.SplitHostPort(hostport)
|
||||
if err != nil {
|
||||
host = hostport
|
||||
strport = fmt.Sprint(defaultPort)
|
||||
}
|
||||
port, err := strconv.Atoi(strport)
|
||||
if err != nil {
|
||||
port = defaultPort
|
||||
}
|
||||
if host == "" {
|
||||
host = "0.0.0.0"
|
||||
}
|
||||
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)},
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
func getAbsoluteFilePath(filename string) string {
|
||||
if filepath.IsAbs(filename) {
|
||||
return filename
|
||||
}
|
||||
wd, _ := os.Getwd()
|
||||
return filepath.Join(wd, filename)
|
||||
}
|
105
internal/controlplane/xds_clusters.go
Normal file
105
internal/controlplane/xds_clusters.go
Normal file
|
@ -0,0 +1,105 @@
|
|||
package controlplane
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
envoy_config_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
|
||||
envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
||||
envoy_config_endpoint_v3 "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
"github.com/pomerium/pomerium/config"
|
||||
"github.com/pomerium/pomerium/internal/urlutil"
|
||||
)
|
||||
|
||||
func (srv *Server) buildClusters(options config.Options) []*envoy_config_cluster_v3.Cluster {
|
||||
grpcURL := &url.URL{
|
||||
Scheme: "grpc",
|
||||
Host: srv.GRPCListener.Addr().String(),
|
||||
}
|
||||
httpURL := &url.URL{
|
||||
Scheme: "http",
|
||||
Host: srv.HTTPListener.Addr().String(),
|
||||
}
|
||||
authzURL := &url.URL{
|
||||
Scheme: strings.Replace(options.AuthorizeURL.Scheme, "http", "grpc", -1),
|
||||
Host: options.AuthorizeURL.Host,
|
||||
}
|
||||
|
||||
clusters := []*envoy_config_cluster_v3.Cluster{
|
||||
srv.buildCluster("pomerium-control-plane-grpc", grpcURL),
|
||||
srv.buildCluster("pomerium-control-plane-http", httpURL),
|
||||
srv.buildCluster("pomerium-authz", authzURL),
|
||||
}
|
||||
|
||||
if config.IsProxy(options.Services) {
|
||||
type clusterDestination struct {
|
||||
name, scheme, hostport string
|
||||
}
|
||||
clusterDestinations := map[clusterDestination]struct{}{}
|
||||
for _, policy := range options.Policies {
|
||||
name, scheme, hostport := srv.getClusterDetails(policy.Destination)
|
||||
clusterDestinations[clusterDestination{name, scheme, hostport}] = struct{}{}
|
||||
}
|
||||
|
||||
for dst := range clusterDestinations {
|
||||
name, scheme, hostport := dst.name, dst.scheme, dst.hostport
|
||||
clusters = append(clusters, srv.buildCluster(name, &url.URL{
|
||||
Scheme: scheme,
|
||||
Host: hostport,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
return clusters
|
||||
}
|
||||
|
||||
func (srv *Server) getClusterDetails(endpoint *url.URL) (name, scheme, hostport string) {
|
||||
name = endpoint.Scheme + "-" + strings.Replace(endpoint.Host, ":", "--", -1)
|
||||
return name, endpoint.Scheme, endpoint.Host
|
||||
}
|
||||
|
||||
func (srv *Server) buildCluster(name string, endpoint *url.URL) *envoy_config_cluster_v3.Cluster {
|
||||
defaultPort := 80
|
||||
if endpoint.Scheme == "https" || endpoint.Scheme == "grpcs" {
|
||||
defaultPort = 443
|
||||
}
|
||||
|
||||
cluster := &envoy_config_cluster_v3.Cluster{
|
||||
Name: name,
|
||||
ConnectTimeout: ptypes.DurationProto(time.Second * 10),
|
||||
LoadAssignment: &envoy_config_endpoint_v3.ClusterLoadAssignment{
|
||||
ClusterName: name,
|
||||
Endpoints: []*envoy_config_endpoint_v3.LocalityLbEndpoints{{
|
||||
LbEndpoints: []*envoy_config_endpoint_v3.LbEndpoint{{
|
||||
HostIdentifier: &envoy_config_endpoint_v3.LbEndpoint_Endpoint{
|
||||
Endpoint: &envoy_config_endpoint_v3.Endpoint{
|
||||
Address: buildAddress(endpoint.Host, defaultPort),
|
||||
},
|
||||
},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
RespectDnsTtl: true,
|
||||
}
|
||||
|
||||
if endpoint.Scheme == "grpc" {
|
||||
cluster.Http2ProtocolOptions = &envoy_config_core_v3.Http2ProtocolOptions{}
|
||||
}
|
||||
|
||||
if endpoint.Scheme == "https" || endpoint.Scheme == "grpcs" {
|
||||
cluster.TransportSocket = &envoy_config_core_v3.TransportSocket{
|
||||
Name: "tls",
|
||||
}
|
||||
}
|
||||
|
||||
if net.ParseIP(urlutil.StripPort(endpoint.Host)) == nil {
|
||||
cluster.ClusterDiscoveryType = &envoy_config_cluster_v3.Cluster_Type{Type: envoy_config_cluster_v3.Cluster_LOGICAL_DNS}
|
||||
} else {
|
||||
cluster.ClusterDiscoveryType = &envoy_config_cluster_v3.Cluster_Type{Type: envoy_config_cluster_v3.Cluster_STATIC}
|
||||
}
|
||||
|
||||
return cluster
|
||||
}
|
269
internal/controlplane/xds_listeners.go
Normal file
269
internal/controlplane/xds_listeners.go
Normal file
|
@ -0,0 +1,269 @@
|
|||
package controlplane
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"sort"
|
||||
|
||||
envoy_config_accesslog_v3 "github.com/envoyproxy/go-control-plane/envoy/config/accesslog/v3"
|
||||
envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
||||
envoy_config_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
|
||||
envoy_config_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
|
||||
envoy_extensions_filters_http_ext_authz_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/ext_authz/v3"
|
||||
envoy_http_connection_manager "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
|
||||
envoy_extensions_transport_sockets_tls_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
|
||||
envoy_type_v3 "github.com/envoyproxy/go-control-plane/envoy/type/v3"
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
"github.com/golang/protobuf/ptypes/any"
|
||||
"github.com/pomerium/pomerium/config"
|
||||
"github.com/pomerium/pomerium/internal/urlutil"
|
||||
)
|
||||
|
||||
var disableExtAuthz *any.Any
|
||||
|
||||
func init() {
|
||||
disableExtAuthz, _ = ptypes.MarshalAny(&envoy_extensions_filters_http_ext_authz_v3.ExtAuthzPerRoute{
|
||||
Override: &envoy_extensions_filters_http_ext_authz_v3.ExtAuthzPerRoute_Disabled{
|
||||
Disabled: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (srv *Server) buildListeners(options config.Options) []*envoy_config_listener_v3.Listener {
|
||||
var listeners []*envoy_config_listener_v3.Listener
|
||||
|
||||
if config.IsAuthenticate(options.Services) || config.IsProxy(options.Services) {
|
||||
listeners = append(listeners, srv.buildHTTPListener(options))
|
||||
}
|
||||
|
||||
if config.IsAuthorize(options.Services) || config.IsCache(options.Services) {
|
||||
listeners = append(listeners, srv.buildGRPCListener(options))
|
||||
}
|
||||
|
||||
return listeners
|
||||
}
|
||||
|
||||
func (srv *Server) buildHTTPListener(options config.Options) *envoy_config_listener_v3.Listener {
|
||||
defaultPort := 80
|
||||
var transportSocket *envoy_config_core_v3.TransportSocket
|
||||
if !options.InsecureServer {
|
||||
defaultPort = 443
|
||||
tlsConfig, _ := ptypes.MarshalAny(srv.buildDownstreamTLSContext(options))
|
||||
transportSocket = &envoy_config_core_v3.TransportSocket{
|
||||
Name: "tls",
|
||||
ConfigType: &envoy_config_core_v3.TransportSocket_TypedConfig{
|
||||
TypedConfig: tlsConfig,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var virtualHosts []*envoy_config_route_v3.VirtualHost
|
||||
for _, domain := range srv.getAllRouteableDomains(options, options.Addr) {
|
||||
vh := &envoy_config_route_v3.VirtualHost{
|
||||
Name: domain,
|
||||
Domains: []string{domain},
|
||||
}
|
||||
|
||||
if options.Addr == options.GRPCAddr {
|
||||
// if this is a gRPC service domain and we're supposed to handle that, add those routes
|
||||
if (config.IsAuthorize(options.Services) && domain == urlutil.StripPort(options.AuthorizeURL.Host)) ||
|
||||
(config.IsCache(options.Services) && domain == urlutil.StripPort(options.CacheURL.Host)) {
|
||||
vh.Routes = append(vh.Routes, srv.buildGRPCRoutes()...)
|
||||
}
|
||||
}
|
||||
|
||||
// these routes match /.pomerium/... and similar paths
|
||||
vh.Routes = append(vh.Routes, srv.buildPomeriumHTTPRoutes(options, domain)...)
|
||||
|
||||
// if we're the proxy, add all the policy routes
|
||||
if config.IsProxy(options.Services) {
|
||||
vh.Routes = append(vh.Routes, srv.buildPolicyRoutes(options, domain)...)
|
||||
}
|
||||
|
||||
if len(vh.Routes) > 0 {
|
||||
virtualHosts = append(virtualHosts, vh)
|
||||
}
|
||||
}
|
||||
|
||||
extAuthZ, _ := ptypes.MarshalAny(&envoy_extensions_filters_http_ext_authz_v3.ExtAuthz{
|
||||
StatusOnError: &envoy_type_v3.HttpStatus{
|
||||
Code: envoy_type_v3.StatusCode_InternalServerError,
|
||||
},
|
||||
Services: &envoy_extensions_filters_http_ext_authz_v3.ExtAuthz_GrpcService{
|
||||
GrpcService: &envoy_config_core_v3.GrpcService{
|
||||
TargetSpecifier: &envoy_config_core_v3.GrpcService_EnvoyGrpc_{
|
||||
EnvoyGrpc: &envoy_config_core_v3.GrpcService_EnvoyGrpc{
|
||||
ClusterName: "pomerium-authz",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
tc, _ := ptypes.MarshalAny(&envoy_http_connection_manager.HttpConnectionManager{
|
||||
CodecType: envoy_http_connection_manager.HttpConnectionManager_AUTO,
|
||||
StatPrefix: "ingress",
|
||||
RouteSpecifier: &envoy_http_connection_manager.HttpConnectionManager_RouteConfig{
|
||||
RouteConfig: &envoy_config_route_v3.RouteConfiguration{
|
||||
Name: "main",
|
||||
VirtualHosts: virtualHosts,
|
||||
},
|
||||
},
|
||||
HttpFilters: []*envoy_http_connection_manager.HttpFilter{
|
||||
{
|
||||
Name: "envoy.filters.http.ext_authz",
|
||||
ConfigType: &envoy_http_connection_manager.HttpFilter_TypedConfig{
|
||||
TypedConfig: extAuthZ,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "envoy.filters.http.router",
|
||||
},
|
||||
},
|
||||
AccessLog: []*envoy_config_accesslog_v3.AccessLog{srv.buildAccessLog()},
|
||||
})
|
||||
|
||||
li := &envoy_config_listener_v3.Listener{
|
||||
Name: "http-ingress",
|
||||
Address: buildAddress(options.Addr, defaultPort),
|
||||
FilterChains: []*envoy_config_listener_v3.FilterChain{{
|
||||
Filters: []*envoy_config_listener_v3.Filter{
|
||||
{
|
||||
Name: "envoy.filters.network.http_connection_manager",
|
||||
ConfigType: &envoy_config_listener_v3.Filter_TypedConfig{
|
||||
TypedConfig: tc,
|
||||
},
|
||||
},
|
||||
},
|
||||
TransportSocket: transportSocket,
|
||||
}},
|
||||
}
|
||||
return li
|
||||
}
|
||||
|
||||
func (srv *Server) buildGRPCListener(options config.Options) *envoy_config_listener_v3.Listener {
|
||||
defaultPort := 80
|
||||
var transportSocket *envoy_config_core_v3.TransportSocket
|
||||
if !options.GRPCInsecure {
|
||||
defaultPort = 443
|
||||
tlsConfig, _ := ptypes.MarshalAny(srv.buildDownstreamTLSContext(options))
|
||||
transportSocket = &envoy_config_core_v3.TransportSocket{
|
||||
Name: "tls",
|
||||
ConfigType: &envoy_config_core_v3.TransportSocket_TypedConfig{
|
||||
TypedConfig: tlsConfig,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
tc, _ := ptypes.MarshalAny(&envoy_http_connection_manager.HttpConnectionManager{
|
||||
CodecType: envoy_http_connection_manager.HttpConnectionManager_AUTO,
|
||||
StatPrefix: "grpc_ingress",
|
||||
RouteSpecifier: &envoy_http_connection_manager.HttpConnectionManager_RouteConfig{
|
||||
RouteConfig: &envoy_config_route_v3.RouteConfiguration{
|
||||
Name: "grpc",
|
||||
VirtualHosts: []*envoy_config_route_v3.VirtualHost{{
|
||||
Name: "grpc",
|
||||
Domains: []string{"*"},
|
||||
Routes: []*envoy_config_route_v3.Route{{
|
||||
Name: "grpc",
|
||||
Match: &envoy_config_route_v3.RouteMatch{
|
||||
PathSpecifier: &envoy_config_route_v3.RouteMatch_Prefix{Prefix: "/"},
|
||||
Grpc: &envoy_config_route_v3.RouteMatch_GrpcRouteMatchOptions{},
|
||||
},
|
||||
Action: &envoy_config_route_v3.Route_Route{
|
||||
Route: &envoy_config_route_v3.RouteAction{
|
||||
ClusterSpecifier: &envoy_config_route_v3.RouteAction_Cluster{Cluster: "pomerium-control-plane-grpc"},
|
||||
},
|
||||
},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
},
|
||||
HttpFilters: []*envoy_http_connection_manager.HttpFilter{{
|
||||
Name: "envoy.filters.http.router",
|
||||
}},
|
||||
})
|
||||
|
||||
return &envoy_config_listener_v3.Listener{
|
||||
Name: "grpc-ingress",
|
||||
Address: buildAddress(options.GRPCAddr, defaultPort),
|
||||
FilterChains: []*envoy_config_listener_v3.FilterChain{{
|
||||
Filters: []*envoy_config_listener_v3.Filter{{
|
||||
Name: "envoy.filters.network.http_connection_manager",
|
||||
ConfigType: &envoy_config_listener_v3.Filter_TypedConfig{
|
||||
TypedConfig: tc,
|
||||
},
|
||||
}},
|
||||
TransportSocket: transportSocket,
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
func (srv *Server) buildDownstreamTLSContext(options config.Options) *envoy_extensions_transport_sockets_tls_v3.DownstreamTlsContext {
|
||||
var cert envoy_extensions_transport_sockets_tls_v3.TlsCertificate
|
||||
if options.Cert != "" {
|
||||
bs, _ := base64.StdEncoding.DecodeString(options.Cert)
|
||||
cert.CertificateChain = &envoy_config_core_v3.DataSource{
|
||||
Specifier: &envoy_config_core_v3.DataSource_InlineBytes{
|
||||
InlineBytes: bs,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
cert.CertificateChain = &envoy_config_core_v3.DataSource{
|
||||
Specifier: &envoy_config_core_v3.DataSource_Filename{
|
||||
Filename: getAbsoluteFilePath(options.CertFile),
|
||||
},
|
||||
}
|
||||
}
|
||||
if options.Key != "" {
|
||||
bs, _ := base64.StdEncoding.DecodeString(options.Key)
|
||||
cert.PrivateKey = &envoy_config_core_v3.DataSource{
|
||||
Specifier: &envoy_config_core_v3.DataSource_InlineBytes{
|
||||
InlineBytes: bs,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
cert.PrivateKey = &envoy_config_core_v3.DataSource{
|
||||
Specifier: &envoy_config_core_v3.DataSource_Filename{
|
||||
Filename: getAbsoluteFilePath(options.KeyFile),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return &envoy_extensions_transport_sockets_tls_v3.DownstreamTlsContext{
|
||||
CommonTlsContext: &envoy_extensions_transport_sockets_tls_v3.CommonTlsContext{
|
||||
TlsCertificates: []*envoy_extensions_transport_sockets_tls_v3.TlsCertificate{
|
||||
&cert,
|
||||
},
|
||||
AlpnProtocols: []string{"h2", "http/1.1"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (srv *Server) getAllRouteableDomains(options config.Options, addr string) []string {
|
||||
lookup := map[string]struct{}{}
|
||||
if config.IsAuthenticate(options.Services) && addr == options.Addr {
|
||||
lookup[urlutil.StripPort(options.AuthenticateURL.Host)] = struct{}{}
|
||||
}
|
||||
if config.IsAuthorize(options.Services) && addr == options.GRPCAddr {
|
||||
lookup[urlutil.StripPort(options.AuthorizeURL.Host)] = struct{}{}
|
||||
}
|
||||
if config.IsCache(options.Services) && addr == options.GRPCAddr {
|
||||
lookup[urlutil.StripPort(options.CacheURL.Host)] = struct{}{}
|
||||
}
|
||||
if config.IsProxy(options.Services) && addr == options.Addr {
|
||||
for _, policy := range options.Policies {
|
||||
lookup[urlutil.StripPort(policy.Source.Host)] = struct{}{}
|
||||
}
|
||||
if options.ForwardAuthURL != nil {
|
||||
lookup[urlutil.StripPort(options.ForwardAuthURL.Host)] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
domains := make([]string, 0, len(lookup))
|
||||
for domain := range lookup {
|
||||
domains = append(domains, domain)
|
||||
}
|
||||
sort.Strings(domains)
|
||||
|
||||
return domains
|
||||
}
|
136
internal/controlplane/xds_routes.go
Normal file
136
internal/controlplane/xds_routes.go
Normal file
|
@ -0,0 +1,136 @@
|
|||
package controlplane
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
envoy_config_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
|
||||
envoy_type_matcher_v3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
|
||||
"github.com/golang/protobuf/ptypes/any"
|
||||
|
||||
"github.com/pomerium/pomerium/config"
|
||||
"github.com/pomerium/pomerium/internal/urlutil"
|
||||
)
|
||||
|
||||
func (srv *Server) buildGRPCRoutes() []*envoy_config_route_v3.Route {
|
||||
action := &envoy_config_route_v3.Route_Route{
|
||||
Route: &envoy_config_route_v3.RouteAction{
|
||||
ClusterSpecifier: &envoy_config_route_v3.RouteAction_Cluster{
|
||||
Cluster: "pomerium-control-plane-grpc",
|
||||
},
|
||||
},
|
||||
}
|
||||
return []*envoy_config_route_v3.Route{{
|
||||
Name: "pomerium-grpc",
|
||||
Match: &envoy_config_route_v3.RouteMatch{
|
||||
PathSpecifier: &envoy_config_route_v3.RouteMatch_Prefix{
|
||||
Prefix: "/",
|
||||
},
|
||||
Grpc: &envoy_config_route_v3.RouteMatch_GrpcRouteMatchOptions{},
|
||||
},
|
||||
Action: action,
|
||||
TypedPerFilterConfig: map[string]*any.Any{
|
||||
"envoy.filters.http.ext_authz": disableExtAuthz,
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
func (srv *Server) buildPomeriumHTTPRoutes(options config.Options, domain string) []*envoy_config_route_v3.Route {
|
||||
routes := []*envoy_config_route_v3.Route{
|
||||
srv.buildControlPlanePathRoute("/ping"),
|
||||
srv.buildControlPlanePathRoute("/healthz"),
|
||||
srv.buildControlPlanePathRoute("/.pomerium"),
|
||||
srv.buildControlPlanePrefixRoute("/.pomerium/"),
|
||||
}
|
||||
// if we're handling authentication, add the oauth2 callback url
|
||||
if config.IsAuthenticate(options.Services) && domain == urlutil.StripPort(options.AuthenticateURL.Host) {
|
||||
routes = append(routes,
|
||||
srv.buildControlPlanePathRoute(options.AuthenticateCallbackPath))
|
||||
}
|
||||
// if we're the proxy and this is the forward-auth url
|
||||
if config.IsProxy(options.Services) && options.ForwardAuthURL != nil && domain == urlutil.StripPort(options.ForwardAuthURL.Host) {
|
||||
routes = append(routes,
|
||||
srv.buildControlPlanePrefixRoute("/"))
|
||||
}
|
||||
return routes
|
||||
}
|
||||
|
||||
func (srv *Server) buildControlPlanePathRoute(path string) *envoy_config_route_v3.Route {
|
||||
return &envoy_config_route_v3.Route{
|
||||
Name: "pomerium-path-" + path,
|
||||
Match: &envoy_config_route_v3.RouteMatch{
|
||||
PathSpecifier: &envoy_config_route_v3.RouteMatch_Path{Path: path},
|
||||
},
|
||||
Action: &envoy_config_route_v3.Route_Route{
|
||||
Route: &envoy_config_route_v3.RouteAction{
|
||||
ClusterSpecifier: &envoy_config_route_v3.RouteAction_Cluster{
|
||||
Cluster: "pomerium-control-plane-http",
|
||||
},
|
||||
},
|
||||
},
|
||||
TypedPerFilterConfig: map[string]*any.Any{
|
||||
"envoy.filters.http.ext_authz": disableExtAuthz,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (srv *Server) buildControlPlanePrefixRoute(prefix string) *envoy_config_route_v3.Route {
|
||||
return &envoy_config_route_v3.Route{
|
||||
Name: "pomerium-prefix-" + prefix,
|
||||
Match: &envoy_config_route_v3.RouteMatch{
|
||||
PathSpecifier: &envoy_config_route_v3.RouteMatch_Prefix{Prefix: prefix},
|
||||
},
|
||||
Action: &envoy_config_route_v3.Route_Route{
|
||||
Route: &envoy_config_route_v3.RouteAction{
|
||||
ClusterSpecifier: &envoy_config_route_v3.RouteAction_Cluster{
|
||||
Cluster: "pomerium-control-plane-http",
|
||||
},
|
||||
},
|
||||
},
|
||||
TypedPerFilterConfig: map[string]*any.Any{
|
||||
"envoy.filters.http.ext_authz": disableExtAuthz,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (srv *Server) buildPolicyRoutes(options config.Options, domain string) []*envoy_config_route_v3.Route {
|
||||
var routes []*envoy_config_route_v3.Route
|
||||
for i, policy := range options.Policies {
|
||||
if policy.Source.Hostname() != domain {
|
||||
continue
|
||||
}
|
||||
|
||||
match := &envoy_config_route_v3.RouteMatch{}
|
||||
switch {
|
||||
case policy.Regex != "":
|
||||
match.PathSpecifier = &envoy_config_route_v3.RouteMatch_SafeRegex{
|
||||
SafeRegex: &envoy_type_matcher_v3.RegexMatcher{
|
||||
EngineType: &envoy_type_matcher_v3.RegexMatcher_GoogleRe2{
|
||||
GoogleRe2: &envoy_type_matcher_v3.RegexMatcher_GoogleRE2{},
|
||||
},
|
||||
Regex: policy.Regex,
|
||||
},
|
||||
}
|
||||
case policy.Path != "":
|
||||
match.PathSpecifier = &envoy_config_route_v3.RouteMatch_Path{Path: policy.Path}
|
||||
case policy.Prefix != "":
|
||||
match.PathSpecifier = &envoy_config_route_v3.RouteMatch_Prefix{Prefix: policy.Prefix}
|
||||
default:
|
||||
match.PathSpecifier = &envoy_config_route_v3.RouteMatch_Prefix{Prefix: "/"}
|
||||
}
|
||||
|
||||
clusterName, _, _ := srv.getClusterDetails(policy.Destination)
|
||||
|
||||
routes = append(routes, &envoy_config_route_v3.Route{
|
||||
Name: fmt.Sprintf("policy-%d", i),
|
||||
Match: match,
|
||||
Action: &envoy_config_route_v3.Route_Route{
|
||||
Route: &envoy_config_route_v3.RouteAction{
|
||||
ClusterSpecifier: &envoy_config_route_v3.RouteAction_Cluster{
|
||||
Cluster: clusterName,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
return routes
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue