github.com/imran-kn/cilium-fork@v1.6.9/proxylib/npds/client.go (about)

     1  // Copyright 2018 Authors of Cilium
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package npds
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"io"
    21  	"time"
    22  
    23  	"github.com/cilium/cilium/pkg/backoff"
    24  	"github.com/cilium/cilium/pkg/lock"
    25  	"github.com/cilium/cilium/proxylib/proxylib"
    26  
    27  	"github.com/cilium/proxy/go/cilium/api"
    28  	envoy_api_v2 "github.com/cilium/proxy/go/envoy/api/v2"
    29  	envoy_api_v2_core "github.com/cilium/proxy/go/envoy/api/v2/core"
    30  	log "github.com/sirupsen/logrus"
    31  	"google.golang.org/genproto/googleapis/rpc/status"
    32  	"google.golang.org/grpc"
    33  )
    34  
    35  const (
    36  	DialDelay    = 100 * time.Millisecond
    37  	BackOffLimit = 100 // Max 100 times DialDelay
    38  	NPDSTypeURL  = "type.googleapis.com/cilium.NetworkPolicy"
    39  )
    40  
    41  type Client struct {
    42  	updater proxylib.PolicyUpdater
    43  	mutex   lock.Mutex
    44  	nodeId  string
    45  	path    string
    46  	conn    *grpc.ClientConn
    47  	stream  grpc.ClientStream
    48  	closing bool
    49  }
    50  
    51  func (c *Client) Close() {
    52  	c.mutex.Lock()
    53  	defer c.mutex.Unlock()
    54  	if !c.closing {
    55  		log.Debugf("NPDS: Client %s closing on %s", c.nodeId, c.path)
    56  		c.closing = true
    57  		if c.stream != nil {
    58  			c.stream.CloseSend()
    59  		}
    60  		if c.conn != nil {
    61  			c.conn.Close()
    62  		}
    63  	}
    64  }
    65  
    66  func (c *Client) Path() string {
    67  	return c.path
    68  }
    69  
    70  func NewClient(path, nodeId string, updater proxylib.PolicyUpdater) proxylib.PolicyClient {
    71  	if path == "" {
    72  		return nil
    73  	}
    74  	c := &Client{
    75  		updater: updater,
    76  		path:    path,
    77  		nodeId:  nodeId,
    78  	}
    79  	log.Debugf("NPDS: Client %s starting on %s", c.nodeId, c.path)
    80  
    81  	// These are used to return error if the 1st try fails
    82  	// Only used for testing and logging, as we keep on trying anyway.
    83  	startErr := make(chan error) // Channel open as long as 'starting == true'
    84  
    85  	BackOff := backoff.Exponential{
    86  		Min:  DialDelay,
    87  		Max:  BackOffLimit * DialDelay,
    88  		Name: "proxylib NPDS client",
    89  	}
    90  
    91  	go func() {
    92  		starting := true
    93  		backOff := BackOff
    94  		for {
    95  			err := c.Run(func() {
    96  				// Report successful start on the first try by closing the channel
    97  				if starting {
    98  					close(startErr)
    99  					starting = false
   100  				}
   101  				log.Debugf("NPDS: Client %s connected on %s", c.nodeId, c.path)
   102  			})
   103  			c.mutex.Lock()
   104  			closing := c.closing
   105  			c.mutex.Unlock()
   106  
   107  			if err != nil {
   108  				log.Debug(err)
   109  				if starting {
   110  					startErr <- err
   111  					close(startErr)
   112  					starting = false
   113  				}
   114  			} else {
   115  				// Reset backoff after successful start
   116  				backOff = BackOff
   117  			}
   118  
   119  			if closing {
   120  				break
   121  			}
   122  
   123  			// Back off before retrying
   124  			backOff.Wait(context.TODO())
   125  		}
   126  	}()
   127  
   128  	// Block until we know if the first connection try succeeded or failed
   129  	_ = <-startErr
   130  	return c
   131  }
   132  
   133  func (c *Client) Run(connected func()) (err error) {
   134  	unixPath := "unix://" + c.path
   135  
   136  	defer func() {
   137  		// Recover from any possible panics
   138  		if r := recover(); r != nil {
   139  			err = fmt.Errorf("NPDS Client %s: Panic: %v", c.nodeId, r)
   140  		}
   141  	}()
   142  
   143  	//
   144  	// WithInsecure() is safe here because we are connecting to a Unix-domain socket,
   145  	// data of whch is never on the wire and security for which can be managed with file permissions.
   146  	//
   147  	conn, err := grpc.Dial(unixPath, grpc.WithInsecure())
   148  	if err != nil {
   149  		return fmt.Errorf("NPDS: Client %s grpc.Dial() on %s failed: %s", c.nodeId, c.path, err)
   150  	}
   151  	client := cilium.NewNetworkPolicyDiscoveryServiceClient(conn)
   152  	stream, err := client.StreamNetworkPolicies(context.Background())
   153  	if err != nil {
   154  		conn.Close()
   155  		return fmt.Errorf("NPDS: Client %s stream failed on %s: %s", c.nodeId, c.path, err)
   156  	}
   157  	c.mutex.Lock()
   158  	c.conn = conn
   159  	c.stream = stream
   160  	c.mutex.Unlock()
   161  	defer func() {
   162  		c.mutex.Lock()
   163  		c.stream.CloseSend()
   164  		c.conn.Close()
   165  		c.mutex.Unlock()
   166  	}()
   167  
   168  	// VersionInfo must be empty as we have not received anything yet.
   169  	// ResourceNames is empty to request for all policies.
   170  	// ResponseNonce is copied from the response, initially empty.
   171  	req := envoy_api_v2.DiscoveryRequest{
   172  		TypeUrl:       NPDSTypeURL,
   173  		VersionInfo:   "",
   174  		Node:          &envoy_api_v2_core.Node{Id: c.nodeId},
   175  		ResourceNames: nil,
   176  		ResponseNonce: "",
   177  	}
   178  	err = stream.Send(&req)
   179  	if err != nil {
   180  		return fmt.Errorf("NPDS: Client %s stream.Send() failed on %s: %s", c.nodeId, c.path, err)
   181  	}
   182  
   183  	connected()
   184  
   185  	for {
   186  		// Receive next policy configuration. This will block until the
   187  		// server has a new version to send, which may take a long time.
   188  		resp, err := stream.Recv()
   189  		if err == io.EOF || err == io.ErrUnexpectedEOF {
   190  			log.Debugf("NPDS: Client %s stream on %s closed.", c.nodeId, c.path)
   191  			break
   192  		}
   193  		if err != nil {
   194  			return fmt.Errorf("NPDS: Client %s stream.Recv() on %s failed: %s", c.nodeId, c.path, err)
   195  		}
   196  
   197  		// Validate the response
   198  		if resp.TypeUrl != req.TypeUrl {
   199  			msg := fmt.Sprintf("NPDS: Client %s rejecting mismatching resource type on %s: %s", c.nodeId, c.path, resp.TypeUrl)
   200  			req.ErrorDetail = &status.Status{Message: msg}
   201  			log.Warning(msg)
   202  		} else {
   203  			err = c.updater.PolicyUpdate(resp)
   204  			if err != nil {
   205  				msg := fmt.Sprintf("NPDS: Client %s rejecting invalid policy on %s: %s", c.nodeId, c.path, err)
   206  				req.ErrorDetail = &status.Status{Message: msg}
   207  				log.Warning(msg)
   208  			} else {
   209  				// Success, update the last applied version
   210  				log.Debugf("NPDS: Client %s acking new policy version on %s: %s", c.nodeId, c.path, resp.VersionInfo)
   211  				req.ErrorDetail = nil
   212  				req.VersionInfo = resp.VersionInfo
   213  			}
   214  		}
   215  		req.ResponseNonce = resp.Nonce
   216  		err = stream.Send(&req)
   217  		if err != nil {
   218  			return fmt.Errorf("NPDS: Client %s stream.Send() failed on %s: %s", c.nodeId, c.path, err)
   219  		}
   220  	}
   221  	return nil
   222  }