Menggunakan UDP dengan Load Balancer Jaringan passthrough eksternal

Dokumen ini membahas cara menggunakan Load Balancer Jaringan passthrough eksternal menggunakan User Datagram Protocol (UDP). Dokumen ini ditujukan untuk developer aplikasi, operator aplikasi, dan administrator jaringan.

Tentang UDP

UDP biasanya digunakan di aplikasi. Protokol ini, yang dijelaskan dalam RFC-768, mengimplementasikan layanan paket datagram stateless yang tidak dapat diandalkan. Misalnya, protokol QUIC Google meningkatkan pengalaman pengguna dengan menggunakan UDP untuk mempercepat aplikasi berbasis streaming.

Bagian stateless UDP berarti lapisan transpor tidak mempertahankan status. Oleh karena itu, setiap paket dalam "koneksi" UDP bersifat independen. Sebenarnya, tidak ada koneksi yang sebenarnya di UDP. Sebagai gantinya, pesertanya biasanya menggunakan tuple 2 (ip:port) atau tuple 4 (src-ip:src-port, dest-ip:dest-port) untuk mengenali satu sama lain.

Seperti aplikasi berbasis TCP, aplikasi berbasis UDP juga dapat memanfaatkan load balancer, yang merupakan alasan Load Balancer Jaringan passthrough eksternal digunakan dalam skenario UDP.

Load Balancer Jaringan passthrough eksternal

Load Balancer Jaringan passthrough eksternal adalah load balancer passthrough; load balancer ini memproses paket masuk dan mengirimkannya ke server backend dengan paket yang utuh. Server backend kemudian mengirimkan paket yang ditampilkan langsung ke klien. Teknik ini disebut Direct Server Return (DSR). Di setiap virtual machine (VM) Linux yang berjalan di Compute Engine yang merupakan backend dari Load Balancer Jaringan passthrough eksternal Google Cloud, entri dalam tabel pemilihan rute lokal merutekan traffic yang ditujukan untuk alamat IP load balancer ke pengontrol antarmuka jaringan (NIC). Contoh berikut menunjukkan teknik ini:

root@backend-server:~# ip ro ls table local
local 10.128.0.2 dev eth0 proto kernel scope host src 10.128.0.2
broadcast 10.128.0.2 dev eth0 proto kernel scope link src 10.128.0.2
local 198.51.100.2 dev eth0 proto 66 scope host
broadcast 127.0.0.0 dev lo proto kernel scope link src 127.0.0.1
local 127.0.0.0/8 dev lo proto kernel scope host src 127.0.0.1
local 127.0.0.1 dev lo proto kernel scope host src 127.0.0.1
broadcast 127.255.255.255 dev lo proto kernel scope link src 127.0.0.1

Pada contoh sebelumnya, 198.51.100.2 adalah alamat IP load balancer. Agen google-network-daemon.service bertanggung jawab untuk menambahkan entri ini. Namun, seperti yang ditunjukkan contoh berikut, VM sebenarnya tidak memiliki antarmuka yang memiliki alamat IP load balancer:

root@backend-server:~# ip ad ls
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1460 qdisc mq state UP group default qlen 1000
    link/ether 42:01:0a:80:00:02 brd ff:ff:ff:ff:ff:ff
    inet 10.128.0.2/32 brd 10.128.0.2 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::4001:aff:fe80:2/64 scope link
       valid_lft forever preferred_lft forever

Load Balancer Jaringan passthrough eksternal mengirimkan paket masuk, dengan alamat tujuan tidak tersentuh, ke server backend. Entri tabel perutean lokal merutekan paket ke proses aplikasi yang benar, dan paket respons dari aplikasi dikirim langsung ke klien.

Diagram berikut menunjukkan cara kerja Load Balancer Jaringan passthrough eksternal. Paket yang masuk diproses oleh load balancer yang disebut Maglev, yang mendistribusikan paket ke server backend. Paket keluar kemudian dikirim langsung ke klien melalui DSR.

Maglev mendistribusikan paket masuk ke server backend, yang mendistribusikan paket melalui DSR.

Masalah terkait paket return UDP

Saat Anda menggunakan DSR, ada sedikit perbedaan antara cara kernel Linux memperlakukan koneksi TCP dan UDP. Karena TCP adalah protokol stateful, kernel memiliki semua informasi yang diperlukan tentang koneksi TCP, termasuk alamat klien, port klien, alamat server, dan port server. Informasi ini dicatat dalam struktur data soket yang mewakili koneksi. Dengan demikian, setiap paket yang ditampilkan dari koneksi TCP memiliki alamat sumber yang ditetapkan dengan benar ke alamat server. Untuk load balancer, alamat tersebut adalah alamat IP load balancer.

Namun, ingat bahwa UDP bersifat stateless, sehingga objek soket yang dibuat dalam proses aplikasi untuk koneksi UDP tidak memiliki informasi koneksi. Kernel tidak memiliki informasi tentang alamat sumber paket keluar, dan tidak mengetahui hubungannya dengan paket yang diterima sebelumnya. Untuk alamat sumber paket, kernel hanya dapat mengisi alamat antarmuka yang dituju paket UDP yang ditampilkan. Atau, jika aplikasi sebelumnya mengikat soket ke alamat tertentu, kernel akan menggunakan alamat tersebut sebagai alamat sumber.

Kode berikut menunjukkan program echo sederhana:

#!/usr/bin/python3
import socket,struct
def loop_on_socket(s):
  while True:
    d, addr = s.recvfrom(1500)
    print(d, addr)
    s.sendto("ECHO: ".encode('utf8')+d, addr)

if __name__ == "__main__":
   HOST, PORT = "0.0.0.0", 60002
   sock = socket.socket(type=socket.SocketKind.SOCK_DGRAM)
   sock.bind((HOST, PORT))
   loop_on_socket(sock)

Berikut adalah output tcpdump selama percakapan UDP:

14:50:04.758029 IP 203.0.113.2.40695 > 198.51.100.2.60002: UDP, length 3
14:50:04.758396 IP 10.128.0.2.60002 > 203.0.113.2.40695: UDP, length 2T

198.51.100.2 adalah alamat IP load balancer, dan 203.0.113.2 adalah alamat IP klien.

Setelah paket keluar dari VM, perangkat NAT lain, yaitu gateway Compute Engine, di jaringan Google Cloud akan menerjemahkan alamat sumber ke alamat eksternal. Gateway tidak mengetahui alamat eksternal mana yang harus digunakan, sehingga hanya alamat eksternal VM (bukan load balancer) yang dapat digunakan.

Dari sisi klien, jika Anda memeriksa output dari tcpdump, paket dari server akan terlihat seperti berikut:

23:05:37.072787 IP 203.0.113.2.40695 > 198.51.100.2.60002: UDP, length 5
23:05:37.344148 IP 198.51.100.3.60002 > 203.0.113.2.40695: UDP, length 4

198.51.100.3 adalah alamat IP eksternal VM.

Dari sudut pandang klien, paket UDP tidak berasal dari alamat yang dituju klien. Hal ini menyebabkan masalah: kernel akan menghapus paket ini, dan jika klien berada di belakang perangkat NAT, begitu juga dengan perangkat NAT. Akibatnya, aplikasi klien tidak mendapatkan respons dari server. Diagram berikut menunjukkan proses ini saat klien menolak paket yang ditampilkan karena ketidakcocokan alamat.

Klien menolak paket yang ditampilkan.

Memecahkan masalah UDP

Untuk mengatasi masalah tidak ada respons, Anda harus menulis ulang alamat sumber paket keluar ke alamat IP load balancer di server yang menghosting aplikasi. Berikut beberapa opsi yang dapat Anda gunakan untuk melakukan penulisan ulang header ini. Solusi pertama menggunakan pendekatan berbasis Linux dengan iptables; solusi lainnya menggunakan pendekatan berbasis aplikasi.

Diagram berikut menunjukkan ide inti dari opsi ini: menulis ulang alamat IP sumber paket yang ditampilkan agar cocok dengan alamat IP load balancer.

Tulis ulang alamat IP sumber paket yang ditampilkan agar cocok dengan alamat IP load balancer.

Menggunakan kebijakan NAT di server backend

Solusi kebijakan NAT adalah menggunakan perintah iptables Linux untuk menulis ulang alamat tujuan dari alamat IP load balancer ke alamat IP VM. Dalam contoh berikut, Anda menambahkan aturan DNAT iptables untuk mengubah alamat tujuan paket yang masuk:

iptables -t nat -A POSTROUTING -j RETURN -d 10.128.0.2 -p udp --dport 60002
iptables -t nat -A PREROUTING -j DNAT --to-destination 10.128.0.2 -d 198.51.100.2 -p udp --dport 60002

Perintah ini menambahkan dua aturan ke tabel NAT sistem iptables. Aturan pertama mengabaikan semua paket masuk yang menargetkan alamat eth0 lokal. Akibatnya, traffic yang tidak berasal dari load balancer tidak akan terpengaruh. Aturan kedua mengubah alamat IP tujuan paket masuk menjadi alamat IP internal VM. Aturan DNAT bersifat stateful, yang berarti kernel melacak koneksi dan menulis ulang alamat sumber paket yang ditampilkan secara otomatis.

Kelebihan Kekurangan
Kernel menerjemahkan alamat, tanpa memerlukan perubahan pada aplikasi. CPU tambahan digunakan untuk melakukan NAT. Dan karena DNAT bersifat stateful, konsumsi memori juga mungkin tinggi.
Mendukung beberapa load balancer.

Menggunakan nftables untuk merusak kolom header IP tanpa status

Dalam solusi nftables, Anda menggunakan perintah nftables untuk merusak alamat sumber di header IP paket keluar. Penggabungan ini stateless, sehingga menggunakan lebih sedikit resource daripada menggunakan DNAT. Untuk menggunakan nftables, Anda memerlukan versi kernel Linux yang lebih besar dari 4.10.

Anda menggunakan perintah berikut:

nft add table raw
nft add chain raw postrouting {type filter hook postrouting priority 300)
nft add rule raw postrouting ip saddr 10.128.0.2 udp sport 60002 ip saddr set 198.51.100.2
Kelebihan Kekurangan
Kernel menerjemahkan alamat, tanpa memerlukan perubahan pada aplikasi. Tidak mendukung beberapa load balancer.
Proses terjemahan alamat bersifat stateless, sehingga konsumsi resource jauh lebih rendah. CPU tambahan digunakan untuk melakukan NAT.
nftables hanya tersedia untuk versi kernel Linux yang lebih baru. Beberapa distro, seperti Centos 7.x, tidak dapat menggunakan nftables.

Izinkan aplikasi secara eksplisit terikat ke alamat IP load balancer

Dalam solusi binding, Anda mengubah aplikasi agar terikat secara eksplisit ke alamat IP load balancer. Untuk soket UDP, operasi bind memungkinkan kernel mengetahui alamat mana yang akan digunakan sebagai alamat sumber saat mengirim paket UDP yang menggunakan soket tersebut.

Contoh berikut menunjukkan cara mengikat ke alamat tertentu di Python:

#!/usr/bin/python3
import socket
def loop_on_socket(s):
  while True:
    d, addr = s.recvfrom(1500)
    print(d, addr)
    s.sendto("ECHO: ".encode('utf8')+d, addr)

if __name__ == "__main__":
   # Instead of setting HOST to "0.0.0.0",
   # we set HOST to the Load Balancer IP
   HOST, PORT = "198.51.100.2", 60002
   sock = socket.socket(type=socket.SocketKind.SOCK_DGRAM)
   sock.bind((HOST, PORT))
   loop_on_socket(sock)

# 198.51.100.2 is the load balancer's IP address
# You can also use the DNS name of the load balancer's IP address

Kode sebelumnya adalah server UDP; kode ini akan memunculkan kembali byte yang diterima, dengan "ECHO: " sebelumnya. Perhatikan baris 12 dan 13, tempat server diikat ke alamat 198.51.100.2, yang merupakan alamat IP load balancer.

Kelebihan Kekurangan
Dapat dicapai dengan perubahan kode sederhana pada aplikasi. Tidak mendukung beberapa load balancer.

Gunakan recvmsg/sendmsg, bukan recvfrom/sendto untuk menentukan alamat

Dalam solusi ini, Anda menggunakan panggilan recvmsg/sendmsg, bukan panggilan recvfrom/sendto. Dibandingkan dengan panggilan recvfrom/sendto, panggilan recvmsg/sendmsg dapat menangani pesan kontrol tambahan beserta data payload. Pesan kontrol tambahan ini mencakup alamat sumber atau tujuan paket. Solusi ini memungkinkan Anda mengambil alamat tujuan dari paket yang masuk, dan karena alamat tersebut adalah alamat load balancer yang sebenarnya, Anda dapat menggunakannya sebagai alamat sumber saat mengirim balasan.

Contoh program berikut menunjukkan solusi ini:

#!/usr/bin/python3
import socket,struct
def loop_on_socket(s):
  while True:
    d, ctl, flg, addr = s.recvmsg(1500, 1024)
    # ctl contains the destination address information
    s.sendmsg(["ECHO: ".encode("utf8"),d], ctl, 0, addr)

if __name__ == "__main__":
   HOST, PORT = "0.0.0.0", 60002
   s = socket.socket(type=socket.SocketKind.SOCK_DGRAM)
   s.setsockopt(0,   # level is 0 (IPPROTO_IP)
                8,   # optname is 8 (IP_PKTINFO)
                1)

   s.bind((HOST, PORT))
   loop_on_socket(s)

Program ini menunjukkan cara menggunakan panggilan recvmsg/sendmsg. Untuk mengambil informasi alamat dari paket, Anda harus menggunakan panggilan setsockopt untuk menetapkan opsi IP_PKTINFO.

Kelebihan Kekurangan
Berfungsi meskipun ada beberapa load balancer–misalnya, saat ada load balancer internal dan eksternal yang dikonfigurasi ke backend yang sama. Memerlukan Anda untuk melakukan perubahan kompleks pada aplikasi. Dalam beberapa kasus, hal ini mungkin tidak dapat dilakukan.

Langkah selanjutnya