ubicloud/model/private_subnet.rb
Furkan Sahin b73b043623 Add Basic Load Balancing
This commit adds the basic Ubicloud load balancer. There are two main
parts of this commit:
1. Control plane implementation for VM <-> Load Balancer interactions.
2. Data plane implementation of the load balancer itself.

The first part involves managing the many-to-many entity relationship
between VMs and load balancers. This relationship is handled through
a join table called `load_balancers_vms`, which is automatically managed
by Sequel. Functions like `load_balancer.add_vm` create the intermediary
data in `DB[:load_balancers_vms]`.

The second part is more complex. The Ubicloud Load Balancer is
integrated into a VM's namespace. Each VM on a host has a dedicated
network namespace used for packet filtering, IPv4 NAT, and various
operational tasks. Our implementation uses nftables definitions within
the namespace. Based on packet descriptions and algorithms, we decide
whether to DNAT the packet to a neighboring VM or accept it on the
current one. We use private networking between VMs to transfer packets
when load balancing is required. Additionally, we support load balancing
for both IPv4 and IPv6. Customers can use the public IPv4 address of any
VM being load balanced. If there is only one VM, we perform port
remapping and leave the packet unchanged.

Our load balancer also supports 2 types of algorithms;
1. round_robin
2. source_hash

The important part to note for a load balancer with multiple VMs is
that, each load balancer node will keep their own round_robin counter or
the source_hash result. Therefore, if the client resolves to different
load balancer node at the DNS level, they may not see the behavior
exactly what the algorithm requires. For example, if they use
round_robin with 2 VMs;
1. The first connection is opened to vm1 which points the customer to
vm1.
2. The second connection  is opened to vm1 which points the customer to
vm2.
3. The third connection is opened to vm2 and it points the customer to
vm2.
As you can see, the expected behavior was vm1, vm2, vm1 but we broke it
because the 3rd time we went to a new load balancer.
2024-07-10 13:17:48 +02:00

83 lines
2.3 KiB
Ruby

# frozen_string_literal: true
require_relative "../model"
class PrivateSubnet < Sequel::Model
many_to_many :vms, join_table: Nic.table_name, left_key: :private_subnet_id, right_key: :vm_id
one_to_many :nics, key: :private_subnet_id
one_to_one :strand, key: :id
many_to_many :firewalls
one_to_many :load_balancers
PRIVATE_SUBNET_RANGES = [
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16"
].freeze
dataset_module Pagination
dataset_module Authorization::Dataset
include Authorization::HyperTagMethods
def hyper_tag_name(project)
"project/#{project.ubid}/location/#{display_location}/private-subnet/#{name}"
end
include Authorization::TaggableMethods
def destroy
DB.transaction do
FirewallsPrivateSubnets.where(private_subnet_id: id).all.each(&:destroy)
super
end
end
def display_location
LocationNameConverter.to_display_name(location)
end
def path
"/location/#{display_location}/private-subnet/#{name}"
end
include ResourceMethods
def self.ubid_to_name(ubid)
ubid.to_s[0..7]
end
def display_state
(state == "waiting") ? "available" : state
end
include SemaphoreMethods
semaphore :destroy, :refresh_keys, :add_new_nic, :update_firewall_rules
def self.random_subnet
PRIVATE_SUBNET_RANGES.sample
end
# Here we are blocking the bottom 4 and top 1 addresses of each subnet
# The bottom first address is called the network address, that must be
# blocked since we use it for routing.
# The very last address is blocked because typically it is used as the
# broadcast address.
# We further block the bottom 3 addresses for future proofing. We may
# use it in future for some other purpose. AWS also does that. Here
# is the source;
# https://docs.aws.amazon.com/vpc/latest/userguide/subnet-sizing.html
def random_private_ipv4
total_hosts = 2**(32 - net4.netmask.prefix_len) - 5
random_offset = SecureRandom.random_number(total_hosts) + 4
addr = net4.nth_subnet(32, random_offset)
return random_private_ipv4 if nics.any? { |nic| nic.private_ipv4.to_s == addr.to_s }
addr
end
def random_private_ipv6
addr = net6.nth_subnet(79, SecureRandom.random_number(2**(79 - net6.netmask.prefix_len) - 2).to_i + 1)
return random_private_ipv6 if nics.any? { |nic| nic.private_ipv6.to_s == addr.to_s }
addr
end
end