Como o Service Mesh consegue um proxy transparente que não invade o código de negócios? Interceptação de tráfego por meio de iptables no Istio

índice

1 O problema do microsserviço tradicional MicroService: descoberta de serviço intrusiva do lado do cliente + LoadBalance

1.1 Descoberta de serviço do lado do cliente + balanceamento de carga

2 Como o Istio consegue o sequestro de tráfego?

2.1 O que precisa ser feito?

2.2 Proxy transparente

2.3 Sidecar

2,4 iptables

2.5 container Init

3 Pergunta: Como julgar o tipo de serviço alvo?

3.1 IP do cluster


1 O problema do microsserviço tradicional MicroService: descoberta de serviço intrusiva do lado do cliente + LoadBalance

1.1 Descoberta de serviço do lado do cliente + balanceamento de carga

Microsserviços tradicionais, descoberta de serviço + código de balanceamento de carga, são acoplados ao código de negócios e executados no mesmo processo que o negócio durante a operação.

Por exemplo, o serviço Tomcat iniciado pelo projeto Springboot, a lógica de negócios é executada neste tomcat e o código de descoberta de serviço e o código de balanceamento de carga após a descoberta de serviço também são executados neste tomcat.

Então, você pode separar o código de negócios e o código de estrutura?

Pode-se perceber que apenas o código de negócios é executado no servidor tomcat e a descoberta de serviço + balanceamento de carga é entregue a outros processos?

A resposta é sim. Coloque a descoberta de serviço + balanceamento de carga em um processo secundário separado, desacople-o do código de negócios e, ao mesmo tempo, obtenha proxy para tráfego de serviço por meio de sequestro de tráfego.

Um dos destaques do projeto do Istio é que os aplicativos antigos podem ser conectados perfeitamente à plataforma Service Mesh sem modificar uma linha de código. Para realizar esta função, atualmente, o tráfego é interceptado e encaminhado para o proxy através do iptables.

2 Como o Istio consegue o sequestro de tráfego?

Com referência à implementação do Istio, podemos projetar um esquema simples de sequestro de tráfego por nós mesmos.

2.1 O que precisa ser feito?

  • Primeiro, deve haver um proxy que ofereça suporte a proxy transparente, que possa lidar com o tráfego sequestrado e ser capaz de obter o endereço de destino original quando a conexão for estabelecida. No k8s, esse proxy é implantado em um pod usando o método secundário e o serviço para sequestrar o tráfego.
  • Sequestrar o tráfego que queremos sequestrar no proxy por meio de iptables. O tráfego do próprio proxy deve ser excluído.
  • Para atingir a intrusão zero, é melhor não modificar a imagem do serviço.No k8s, o container Init pode ser usado para modificar o iptables antes do container do aplicativo iniciar.

2.2 Proxy transparente

Como um proxy transparente, o tráfego que ele pode manipular passará por uma série de lógicas de processamento, incluindo nova tentativa, tempo limite, balanceamento de carga etc., e então o encaminhará ao serviço de mesmo nível. Para o tráfego que não pode ser processado por si mesmo, ele será transmitido diretamente de forma transparente sem processamento.

Depois que o tráfego é encaminhado ao proxy por meio do iptables, o proxy precisa ser capaz de obter o endereço de destino original quando a conexão for estabelecida. A implementação em Go é um pouco mais problemática e precisa ser obtida  syscall chamando,

Código de amostra:

package redirect

import (
    "errors"
    "fmt"
    "net"
    "os"
    "syscall"
)

const SO_ORIGINAL_DST = 80

var (
    ErrGetSocketoptIPv6 = errors.New("get socketopt ipv6 error")
    ErrResolveTCPAddr   = errors.New("resolve tcp address error")
    ErrTCPConn          = errors.New("not a valid TCPConn")
)

// For transparent proxy.
// Get REDIRECT package's originial dst address.
// Note: it may be only support linux.
func GetOriginalDstAddr(conn *net.TCPConn) (addr net.Addr, c *net.TCPConn, err error) {
    fc, errRet := conn.File()
    if errRet != nil {
        conn.Close()
        err = ErrTCPConn
        return
    } else {
        conn.Close()
    }
    defer fc.Close()

    mreq, errRet := syscall.GetsockoptIPv6Mreq(int(fc.Fd()), syscall.IPPROTO_IP, SO_ORIGINAL_DST)
    if errRet != nil {
        err = ErrGetSocketoptIPv6
        c, _ = getTCPConnFromFile(fc)
        return
    }

    // only support ipv4
    ip := net.IPv4(mreq.Multiaddr[4], mreq.Multiaddr[5], mreq.Multiaddr[6], mreq.Multiaddr[7])
    port := uint16(mreq.Multiaddr[2])<<8 + uint16(mreq.Multiaddr[3])
    addr, err = net.ResolveTCPAddr("tcp4", fmt.Sprintf("%s:%d", ip.String(), port))
    if err != nil {
        err = ErrResolveTCPAddr
        return
    }

    c, errRet = getTCPConnFromFile(fc)
    if errRet != nil {
        err = ErrTCPConn
        return
    }
    return
}

func getTCPConnFromFile(f *os.File) (*net.TCPConn, error) {
    newConn, err := net.FileConn(f)
    if err != nil {
        return nil, ErrTCPConn
    }

    c, ok := newConn.(*net.TCPConn)
    if !ok {
        return nil, ErrTCPConn
    }
    return c, nil
}

O GetOriginalDstAddr endereço de destino original da conexão pode ser obtido por meio da  função.

É importante notar aqui que quando o encaminhamento de iptables está habilitado, se o proxy receber uma conexão que se acesse diretamente, ele reconhecerá que não pode tratá-la e, em seguida, se conectará ao endereço de destino (ou seja, o endereço vinculado a ele mesmo) , para que leve a um loop infinito. Portanto, quando o serviço é iniciado, a conexão cujo endereço de destino é o seu próprio IP precisa ser desconectada diretamente.

2.3 Sidecar

Ao usar o modo Sidecar para implantar a grade de serviço, um proxy adicional será aberto próximo a cada serviço para assumir parte do tráfego do contêiner. No kubernetes, um pod pode ter vários contêineres. Esses vários contêineres podem compartilhar recursos, como rede e armazenamento. Implante conceitualmente o contêiner de serviço e o contêiner de proxy em um pod, e o contêiner de proxy é equivalente a um contêiner de arquivo secundário.

Demonstramos por meio de uma implantação. A configuração yaml desta implantação inclui dois contêineres de teste e proxy, que compartilham a rede, portanto, após fazer login no contêiner de teste,  127.0.0.1:30000 você pode acessar o contêiner de proxy por meio.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: test
  namespace: default
  labels:
    app: test
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: test
    spec:
      containers:
      - name: test
        image: {test-image}
        ports:
          - containerPort: 9100
      - name: proxy
        image: {proxy-image}
        ports:
          - containerPort: 30000    

Escrever a configuração do contêiner do arquivo secundário para cada serviço é uma tarefa complicada. Quando a arquitetura estiver madura, podemos usar as MutatingAdmissionWebhook funções do kubernetes para injetar ativamente configurações relacionadas ao arquivo secundário  quando o usuário criar uma implantação.

Por exemplo, adicionamos os seguintes campos às anotações da implantação:

annotations:
  xxx.com/sidecar.enable: "true"
  xxx.com/sidecar.version: "v1"

Indica que a versão v1 do arquivo secundário precisa ser injetada nesta implantação. Quando nosso serviço recebe este webhook, ele pode verificar o campo de anotações relevantes e decidir se injeta a configuração do arquivo secundário e qual versão da configuração de acordo com a configuração do campo. Se houver alguns parâmetros que precisam ser alterados de acordo com o serviço, você também pode usar isso. A forma de entrega melhora muito a flexibilidade.

2,4 iptables

Por meio do iptables, podemos sequestrar o tráfego especificado para o proxy e excluir algum tráfego.

iptables -t nat -A OUTPUT -p tcp -m owner --uid-owner 9527 -j RETURN
iptables -t nat -A OUTPUT -p tcp -d 172.17.0.0/16 -j REDIRECT --to-port 30000

O comando acima significa transferir 172.17.0.0/16 o tráfego com  o endereço de destino  REDIRECT para a porta 30000 (a porta que o proxy monitora). Mas o processo iniciado com UID 9527 é exceção. 172.17.0.0/16 Este endereço é o segmento IP dentro do cluster k8s. Precisamos apenas sequestrar esta parte do tráfego. Para o tráfego que acessa o exterior do cluster, não o sequestraremos temporariamente. Se todo o tráfego for sequestrado, as solicitações que o o proxy não pode manipular deve ser excluído por meio das regras do iptables.

2.5 container Init

Conforme mencionado anteriormente, a fim de atingir a intrusão zero, precisamos usar o container Init para modificar o iptables antes de iniciar o container de serviço do usuário. Essa parte da configuração também pode ser MutatingAdmissionWebhook injetada na configuração de implantação do usuário por meio da função de kubernetes  .

Adicione a configuração do contêiner Init à configuração do arquivo secundário anterior:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: test
  namespace: default
  labels:
    app: test
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: test
    spec:
      initContainers:
      - name: iptables-init
        image: {iptables-image}
        imagePullPolicy: IfNotPresent
        command: ['sh', '-c', 'iptables -t nat -A OUTPUT -p tcp -m owner --uid-owner 9527 -j RETURN && iptables -t nat -A OUTPUT -p tcp -d 172.17.0.0/16 -j REDIRECT --to-port 30000']
        securityContext:
          capabilities:
            add:
            - NET_ADMIN
          privileged: true
      containers:
      - name: test
        image: {test-image}
        ports:
          - containerPort: 9100
      - name: proxy
        image: {proxy-image}
        ports:
          - containerPort: 30000    

Este container Init precisa instalar o iptables, e o comando iptables que configuramos será executado quando for iniciado.

Precisa de atenção extra é  securityContext este item de configuração, adicionamos  NET_ADMIN permissões. É usado para definir as permissões de Pod ou Container.Se não estiver configurado, o iptables irá avisar um erro ao executar o comando.

3 Pergunta: Como julgar o tipo de serviço alvo?

Nós  172.17.0.0/16 sequestramos todo o tráfego para o proxy, então como determinar o tipo de protocolo do serviço de destino? Se você não souber o tipo de protocolo, não terá certeza de como analisar as solicitações subsequentes.

No serviço kubernetes, podemos especificar um nome para a porta de cada serviço. O formato desse nome pode ser corrigido para  {name}-{protocol}, por exemplo  {test-http}, que uma determinada porta desse serviço seja um protocolo http.

kind: Service
apiVersion: v1
metadata:
  name: test
  namespace: default
spec:
  selector:
    app: test
  ports:
    - name: test-http
      port: 9100
      targetPort: 9100
      protocol: TCP

O proxy obtém o IP do cluster e o nome da porta correspondente ao serviço por meio do serviço de descoberta, para que o tipo de protocolo de comunicação da conexão possa ser conhecido por meio do IP e da porta do serviço de destino, que pode então ser entregue ao manipulador correspondente Para processamento.

3.1 IP do cluster

Crie um serviço em kubernetes. Se não for especificado, o IP do cluster é usado para acessar por padrão. O Kube-proxy criará uma regra de iptables para isso, converterá o IP do cluster em balanceamento de carga e o encaminhará para o IP do pod.

Quando houver um IP do Cluster, a resolução DNS do serviço apontará para o IP do Cluster e o balanceamento de carga será feito pelo iptables. Se não existir, o resultado da resolução DNS apontará diretamente para o IP do pod.

O proxy depende do IP do cluster do serviço para determinar qual serviço o usuário está acessando, portanto, não pode ser definido  clusterIP: None. Como o IP do pod provavelmente muda com frequência, ao adicionar ou subtrair instâncias, o conjunto de IP do pod mudará e o proxy não poderá obter essas mudanças em tempo real.

Acho que você gosta

Origin blog.csdn.net/hugo_lei/article/details/106941516
Recomendado
Clasificación