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.
83 lines
2.3 KiB
Ruby
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
|