Skip to content

Commit a5d0fb5

Browse files
committed
Add a unique VNI to each VPC
1 parent 2835615 commit a5d0fb5

File tree

14 files changed

+208
-118
lines changed

14 files changed

+208
-118
lines changed

Cargo.lock

Lines changed: 7 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

common/src/api/external/mod.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1677,6 +1677,62 @@ impl JsonSchema for MacAddr {
16771677
}
16781678
}
16791679

1680+
/// A Geneve Virtual Network Identifier
1681+
#[derive(
1682+
Debug,
1683+
Clone,
1684+
Copy,
1685+
PartialEq,
1686+
Eq,
1687+
Hash,
1688+
PartialOrd,
1689+
Ord,
1690+
Deserialize,
1691+
Serialize,
1692+
JsonSchema,
1693+
)]
1694+
pub struct Vni(u32);
1695+
1696+
impl Vni {
1697+
const MAX_VNI: u32 = 1 << 24;
1698+
1699+
/// Create a new random VNI.
1700+
pub fn random() -> Self {
1701+
use rand::Rng;
1702+
Self(rand::thread_rng().gen_range(0..=Self::MAX_VNI))
1703+
}
1704+
}
1705+
1706+
impl From<Vni> for u32 {
1707+
fn from(vni: Vni) -> u32 {
1708+
vni.0
1709+
}
1710+
}
1711+
1712+
impl TryFrom<u32> for Vni {
1713+
type Error = Error;
1714+
1715+
fn try_from(x: u32) -> Result<Self, Error> {
1716+
if x <= Self::MAX_VNI {
1717+
Ok(Self(x))
1718+
} else {
1719+
Err(Error::internal_error(
1720+
format!("Invalid Geneve VNI: {}", x).as_str(),
1721+
))
1722+
}
1723+
}
1724+
}
1725+
1726+
impl TryFrom<i32> for Vni {
1727+
type Error = Error;
1728+
1729+
fn try_from(x: i32) -> Result<Self, Error> {
1730+
Self::try_from(u32::try_from(x).map_err(|_| {
1731+
Error::internal_error(format!("Invalid Geneve VNI: {}", x).as_str())
1732+
})?)
1733+
}
1734+
}
1735+
16801736
/// A `NetworkInterface` represents a virtual network interface device.
16811737
#[derive(ObjectIdentity, Clone, Debug, Deserialize, JsonSchema, Serialize)]
16821738
pub struct NetworkInterface {

common/src/sql/dbinit.sql

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,13 @@ CREATE TABLE omicron.public.vpc (
592592
system_router_id UUID NOT NULL,
593593
dns_name STRING(63) NOT NULL,
594594

595+
/*
596+
* The Geneve Virtual Network Identifier for this VPC. Note that this is a
597+
* 24-bit unsigned value, properties which are checked in the application,
598+
* not the database.
599+
*/
600+
vni INT4 NOT NULL,
601+
595602
/* The IPv6 prefix allocated to subnets. */
596603
ipv6_prefix INET NOT NULL,
597604

@@ -606,6 +613,11 @@ CREATE UNIQUE INDEX ON omicron.public.vpc (
606613
) WHERE
607614
time_deleted IS NULL;
608615

616+
CREATE UNIQUE INDEX ON omicron.public.vpc (
617+
vni
618+
) WHERE
619+
time_deleted IS NULL;
620+
609621
CREATE TABLE omicron.public.vpc_subnet (
610622
/* Identity metadata (resource) */
611623
id UUID PRIMARY KEY,

nexus/src/db/datastore.rs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1368,7 +1368,7 @@ impl DataStore {
13681368
}
13691369

13701370
/// Return the information about an instance's network interfaces required
1371-
/// for the sled agent to instantiate it via OPTE.
1371+
/// for the sled agent to instantiate them via OPTE.
13721372
///
13731373
/// OPTE requires information that's currently split across the network
13741374
/// interface and VPC subnet tables. This query just joins those for each
@@ -1381,6 +1381,7 @@ impl DataStore {
13811381
opctx.authorize(authz::Action::ListChildren, authz_instance).await?;
13821382

13831383
use db::schema::network_interface;
1384+
use db::schema::vpc;
13841385
use db::schema::vpc_subnet;
13851386

13861387
// The record type for the results of the below JOIN query
@@ -1391,7 +1392,7 @@ impl DataStore {
13911392
mac: db::model::MacAddr,
13921393
ipv4_block: db::model::Ipv4Net,
13931394
ipv6_block: db::model::Ipv6Net,
1394-
vpc_id: Uuid,
1395+
vni: db::model::Vni,
13951396
slot: i16,
13961397
}
13971398

@@ -1407,7 +1408,7 @@ impl DataStore {
14071408
ip: nic.ip.ip().to_string(),
14081409
mac: sled_client_types::MacAddr::from(nic.mac.0),
14091410
subnet: sled_client_types::IpNet::from(ip_subnet),
1410-
vpc_id: nic.vpc_id,
1411+
vni: sled_client_types::Vni::from(nic.vni.0),
14111412
slot: u8::try_from(nic.slot).unwrap(),
14121413
}
14131414
}
@@ -1420,17 +1421,18 @@ impl DataStore {
14201421
vpc_subnet::table
14211422
.on(network_interface::subnet_id.eq(vpc_subnet::id)),
14221423
)
1424+
.inner_join(vpc::table.on(vpc_subnet::vpc_id.eq(vpc::id)))
14231425
.order_by(network_interface::slot)
14241426
// TODO-cleanup: Having to specify each column again is less than
1425-
// ideal. However, this type doesn't appear to have the `Row`
1426-
// associated type in the documentation. DRY this out.
1427+
// ideal, but we can't derive `Selectable` since this is the result
1428+
// of a JOIN and not from a single table. DRY this out if possible.
14271429
.select((
14281430
network_interface::name,
14291431
network_interface::ip,
14301432
network_interface::mac,
14311433
vpc_subnet::ipv4_block,
14321434
vpc_subnet::ipv6_block,
1433-
network_interface::vpc_id,
1435+
vpc::vni,
14341436
network_interface::slot,
14351437
))
14361438
.get_results_async::<NicInfo>(self.pool_authorized(opctx).await?)

nexus/src/db/model.rs

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -552,15 +552,25 @@ pub struct MacAddr(pub external::MacAddr);
552552
impl MacAddr {
553553
/// Generate a unique MAC address for an interface
554554
pub fn new() -> Result<Self, external::Error> {
555-
use rand::Fill;
556-
// Use the Oxide OUI A8 40 25
557-
let mut addr = [0xA8, 0x40, 0x25, 0x00, 0x00, 0x00];
558-
addr[3..].try_fill(&mut StdRng::from_entropy()).map_err(|_| {
559-
external::Error::internal_error("failed to generate MAC")
560-
})?;
561-
// From RFD 174, Oxide virtual MACs are constrained to have these bits
562-
// set.
563-
addr[3] |= 0xF0;
555+
// From RFD 174, the last three octets of Oxide virtual MACs are
556+
// constrained to be in the range F0:00:00 - FF:FF:FF. However, we're
557+
// further splitting this into a range for customer instances, F0:00:00
558+
// - EF:FF:FF, and VPC / system MAC addresses, FF:00:00 - FF:FF:FF.
559+
// See
560+
// https://github.com/oxidecomputer/omicron/pull/955#discussion_r856432498
561+
// for the initial discussion surrounding this split.
562+
//
563+
// TODO-completeness: Update this comment point to the relevant RFD(s)
564+
// once they're created.
565+
use rand::Rng;
566+
const MIN: u32 = 0x00_00_00;
567+
const MAX: u32 = 0x0E_FF_FF;
568+
let bytes = rand::thread_rng().gen_range(MIN..=MAX).to_be_bytes();
569+
570+
// Use the Oxide OUI A8 40 25, and set the first nibble of the third
571+
// byte, to ensure we're in the virtual address range.
572+
let addr = [0xA8, 0x40, 0x25, 0xF0 | bytes[1], bytes[2], bytes[3]];
573+
564574
// TODO-correctness: We should use an explicit allocator for the MACs
565575
// given the small address space. Right now creation requests may fail
566576
// due to MAC collision, especially given the 20-bit space.
@@ -1848,6 +1858,35 @@ impl OximeterInfo {
18481858
}
18491859
}
18501860

1861+
#[derive(Clone, Debug, Copy, AsExpression, FromSqlRow)]
1862+
#[diesel(sql_type = sql_types::Int4)]
1863+
pub struct Vni(pub external::Vni);
1864+
1865+
impl<DB> ToSql<sql_types::Int4, DB> for Vni
1866+
where
1867+
DB: Backend<BindCollector = diesel::query_builder::bind_collector::RawBytesBindCollector<DB>>,
1868+
i32: ToSql<sql_types::Int4, DB>,
1869+
{
1870+
fn to_sql<'b>(
1871+
&'b self,
1872+
out: &mut serialize::Output<'b, '_, DB>,
1873+
) -> serialize::Result {
1874+
// Reborrowing is necessary to ensure that the lifetime of the temporary
1875+
// i32 created here and `out` is the same, i.e., that `'b = '_`.
1876+
i32::try_from(u32::from(self.0)).unwrap().to_sql(&mut out.reborrow())
1877+
}
1878+
}
1879+
1880+
impl<DB> FromSql<sql_types::Int4, DB> for Vni
1881+
where
1882+
DB: Backend,
1883+
i32: FromSql<sql_types::Int4, DB>,
1884+
{
1885+
fn from_sql(bytes: RawValue<DB>) -> deserialize::Result<Self> {
1886+
Ok(Vni(external::Vni::try_from(i32::from_sql(bytes)?)?))
1887+
}
1888+
}
1889+
18511890
#[derive(Queryable, Insertable, Clone, Debug, Selectable, Resource)]
18521891
#[diesel(table_name = vpc)]
18531892
pub struct Vpc {
@@ -1856,6 +1895,7 @@ pub struct Vpc {
18561895

18571896
pub project_id: Uuid,
18581897
pub system_router_id: Uuid,
1898+
pub vni: Vni,
18591899
pub ipv6_prefix: Ipv6Net,
18601900
pub dns_name: Name,
18611901

@@ -1890,6 +1930,7 @@ impl Vpc {
18901930
identity,
18911931
project_id,
18921932
system_router_id,
1933+
vni: Vni(external::Vni::random()),
18931934
ipv6_prefix,
18941935
dns_name: params.dns_name.into(),
18951936
firewall_gen: Generation::new(),
@@ -2709,6 +2750,7 @@ impl UpdateAvailableArtifact {
27092750

27102751
#[cfg(test)]
27112752
mod tests {
2753+
use super::MacAddr;
27122754
use super::Uuid;
27132755
use super::VpcSubnet;
27142756
use ipnetwork::Ipv4Network;
@@ -2830,4 +2872,19 @@ mod tests {
28302872
assert!(!subnet.check_requestable_addr("fd00::1".parse().unwrap()));
28312873
assert!(subnet.check_requestable_addr("fd00::1:1".parse().unwrap()));
28322874
}
2875+
2876+
#[test]
2877+
fn test_random_mac_address() {
2878+
let mac = MacAddr::new().expect("Failed to create random MAC");
2879+
let bytes = mac.0.as_bytes();
2880+
assert_eq!(&bytes[..3], &[0xA8, 0x40, 0x25], "Invalid Oxide OUI");
2881+
assert_eq!(
2882+
bytes[3] & 0xF0,
2883+
0xF0,
2884+
concat!(
2885+
"Customer MAC addresses should be in the virtual range ",
2886+
"a8:40:25:f0:00:00 - a8:40:25:fe:ff:ff"
2887+
)
2888+
);
2889+
}
28332890
}

nexus/src/db/schema.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,7 @@ table! {
333333
time_deleted -> Nullable<Timestamptz>,
334334
project_id -> Uuid,
335335
system_router_id -> Uuid,
336+
vni -> Int4,
336337
ipv6_prefix -> Inet,
337338
dns_name -> Text,
338339
firewall_gen -> Int8,

nexus/src/nexus.rs

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1850,23 +1850,6 @@ impl Nexus {
18501850
.derive_guest_network_interface_info(&opctx, &authz_instance)
18511851
.await?;
18521852

1853-
/*
1854-
.instance_list_network_interfaces(
1855-
&opctx,
1856-
&authz_instance,
1857-
&DataPageParams {
1858-
marker: None,
1859-
direction: dropshot::PaginationOrder::Ascending,
1860-
limit: std::num::NonZeroU32::new(MAX_NICS_PER_INSTANCE)
1861-
.unwrap(),
1862-
},
1863-
)
1864-
.await?
1865-
.iter()
1866-
.map(|x| x.clone().into())
1867-
.collect();
1868-
*/
1869-
18701853
// Ask the sled agent to begin the state change. Then update the
18711854
// database to reflect the new intermediate state. If this update is
18721855
// not the newest one, that's fine. That might just mean the sled agent

openapi/sled-agent.json

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -915,7 +915,7 @@
915915
"maxLength": 63
916916
},
917917
"NetworkInterface": {
918-
"description": "Information required to construct virtual network interfaces for a guest",
918+
"description": "Information required to construct a virtual network interface for a guest",
919919
"type": "object",
920920
"properties": {
921921
"ip": {
@@ -936,9 +936,8 @@
936936
"subnet": {
937937
"$ref": "#/components/schemas/IpNet"
938938
},
939-
"vpc_id": {
940-
"type": "string",
941-
"format": "uuid"
939+
"vni": {
940+
"$ref": "#/components/schemas/Vni"
942941
}
943942
},
944943
"required": [
@@ -947,7 +946,7 @@
947946
"name",
948947
"slot",
949948
"subnet",
950-
"vpc_id"
949+
"vni"
951950
]
952951
},
953952
"ServiceEnsureBody": {
@@ -1017,6 +1016,12 @@
10171016
"zone"
10181017
]
10191018
},
1019+
"Vni": {
1020+
"description": "A Geneve Virtual Network Identifier",
1021+
"type": "integer",
1022+
"format": "uint32",
1023+
"minimum": 0
1024+
},
10201025
"VolumeConstructionRequest": {
10211026
"oneOf": [
10221027
{

0 commit comments

Comments
 (0)