Skip to content

Commit 1e4ebff

Browse files
authored
Add support for multiple interfaces (#102)
* Add support for multiple interfaces * Broadcast speed should be set per interface
1 parent 05d619d commit 1e4ebff

7 files changed

Lines changed: 178 additions & 39 deletions

File tree

.github/workflows/rust.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ jobs:
4747
sudo apt-get install -y dnsmasq qemu-system-x86 ovmf
4848
sudo mkdir -p /etc/qemu
4949
echo allow br-pixie | sudo tee /etc/qemu/bridge.conf
50+
echo allow br-pixie1 | sudo tee -a /etc/qemu/bridge.conf
5051
5152
- name: Install rust
5253
uses: actions-rust-lang/setup-rust-toolchain@v1

pixie-server/example.config.yaml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
hosts:
2-
listen_on: 10.0.0.1
3-
dhcp: !static [10.187.100.1, 10.187.200.200]
4-
#dhcp: !proxy 192.168.1.100
2+
interfaces:
3+
- network: 10.0.0.1/8
4+
dhcp: !static [10.187.100.1, 10.187.200.200]
5+
#dhcp: !proxy 192.168.1.100
6+
broadcast_speed: 52428800
57
hostsfile: /etc/hosts
6-
broadcast_speed: 52428800
78
http:
89
listen_on: 0.0.0.0:8080
910
#password: secret

pixie-server/src/dnsmasq.rs

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,29 +35,48 @@ impl Drop for DnsmasqHandle {
3535
}
3636

3737
async fn write_config(state: &State) -> Result<()> {
38-
let (name, _) = find_network(state.config.hosts.listen_on)?;
39-
4038
let mut dnsmasq_conf = File::create(state.run_dir.join("dnsmasq.conf"))?;
4139

42-
let dhcp_dynamic_conf = match state.config.hosts.dhcp {
43-
DhcpMode::Static(low, high) => format!("dhcp-range=tag:netboot,{low},{high}"),
44-
DhcpMode::Proxy(ip) => format!("dhcp-range=tag:netboot,{ip},proxy"),
45-
};
46-
4740
let storage_str = state.storage_dir.to_str().unwrap();
4841
let run_str = state.run_dir.to_str().unwrap();
4942

43+
let interfaces_config = state
44+
.config
45+
.hosts
46+
.interfaces
47+
.iter()
48+
.map(|iface| {
49+
let name = find_network(iface.network.addr())?.0;
50+
51+
let dhcp_dynamic_conf = match iface.dhcp {
52+
DhcpMode::Static(low, high) => format!("dhcp-range=tag:netboot,{low},{high}"),
53+
DhcpMode::Proxy(ip) => format!("dhcp-range=tag:netboot,{ip},proxy"),
54+
};
55+
56+
let netaddr = iface.network.network().to_string();
57+
let netmask = iface.network.netmask().to_string();
58+
59+
Ok(format!(
60+
r#"
61+
## {name}
62+
dhcp-range=tag:!netboot,{netaddr},static,{netmask}
63+
{dhcp_dynamic_conf}
64+
interface={name}
65+
"#
66+
))
67+
})
68+
.collect::<Result<Vec<_>>>()?
69+
.join("\n");
70+
5071
write!(
5172
dnsmasq_conf,
5273
r#"
5374
### Per-network configuration
5475
55-
## net0
56-
{dhcp_dynamic_conf}
57-
dhcp-range=tag:!netboot,10.0.0.0,static,255.0.0.0
76+
{interfaces_config}
77+
5878
dhcp-hostsfile={run_str}/hosts
5979
dhcp-boot=pixie-uefi.efi
60-
interface={name}
6180
except-interface=lo
6281
user=root
6382
group=root

pixie-server/src/udp.rs

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
//! Handles [`UdpRequest`]
22
33
use crate::{
4-
find_mac, find_network,
4+
find_mac,
55
state::{State, UnitSelector},
66
};
77
use anyhow::{ensure, Context, Result};
8+
use futures::FutureExt;
9+
use ipnet::Ipv4Net;
810
use pixie_shared::{
9-
chunk_codec::Encoder, ChunkHash, HintPacket, RegistrationInfo, UdpRequest, ACTION_PORT,
10-
CHUNKS_PORT, HINT_PORT, UDP_BODY_LEN,
11+
chunk_codec::Encoder, ChunkHash, HintPacket, InterfaceConfig, RegistrationInfo, UdpRequest,
12+
ACTION_PORT, CHUNKS_PORT, HINT_PORT, UDP_BODY_LEN,
1113
};
1214
use std::{
1315
collections::BTreeSet,
14-
net::{Ipv4Addr, SocketAddrV4},
16+
net::{IpAddr, Ipv4Addr, SocketAddrV4},
1517
ops::Bound,
1618
sync::Arc,
1719
};
@@ -24,7 +26,7 @@ use tokio::{
2426
async fn broadcast_chunks(
2527
state: &State,
2628
socket: &UdpSocket,
27-
ip: Ipv4Addr,
29+
iface: &InterfaceConfig,
2830
mut rx: Receiver<ChunkHash>,
2931
) -> Result<()> {
3032
let mut queue = BTreeSet::<ChunkHash>::new();
@@ -75,8 +77,7 @@ async fn broadcast_chunks(
7577
continue;
7678
};
7779

78-
let hosts_cfg = &state.config.hosts;
79-
let chunks_addr = SocketAddrV4::new(ip, CHUNKS_PORT);
80+
let chunks_addr = SocketAddrV4::new(iface.network.broadcast(), CHUNKS_PORT);
8081

8182
let mut encoder = Encoder::new(cdata);
8283
write_buf[..32].clone_from_slice(&index);
@@ -85,7 +86,7 @@ async fn broadcast_chunks(
8586

8687
let sent_len = socket.send_to(&write_buf[..32 + len], chunks_addr).await?;
8788
ensure!(sent_len == 32 + len, "Could not send packet");
88-
wait_for += 8 * (sent_len as u32) * Duration::from_secs(1) / hosts_cfg.broadcast_speed;
89+
wait_for += 8 * (sent_len as u32) * Duration::from_secs(1) / iface.broadcast_speed;
8990
}
9091
}
9192

@@ -165,13 +166,24 @@ async fn broadcast_hint(state: &State, socket: &UdpSocket, ip: Ipv4Addr) -> Resu
165166
Ok(())
166167
}
167168

168-
async fn handle_requests(state: &State, socket: &UdpSocket, tx: Sender<[u8; 32]>) -> Result<()> {
169+
async fn handle_requests(
170+
state: &State,
171+
socket: &UdpSocket,
172+
net_tx: Vec<(Ipv4Net, Sender<[u8; 32]>)>,
173+
) -> Result<()> {
169174
let mut buf = [0; UDP_BODY_LEN];
170175
loop {
171176
let (len, peer_addr) = tokio::select! {
172177
x = socket.recv_from(&mut buf) => x?,
173178
_ = state.cancel_token.cancelled() => break,
174179
};
180+
let peer_ip = match peer_addr.ip() {
181+
IpAddr::V4(ip) => ip,
182+
_ => panic!(),
183+
};
184+
let Some((_, tx)) = net_tx.iter().find(|(net, _)| net.contains(&peer_ip)) else {
185+
continue;
186+
};
175187
let req: postcard::Result<UdpRequest> = postcard::from_bytes(&buf[..len]);
176188
match req {
177189
Ok(UdpRequest::Discover) => {
@@ -201,18 +213,29 @@ async fn handle_requests(state: &State, socket: &UdpSocket, tx: Sender<[u8; 32]>
201213
}
202214

203215
pub async fn main(state: Arc<State>) -> Result<()> {
204-
let (_, network) = find_network(state.config.hosts.listen_on)?;
216+
let (net_tx, net_rx): (_, Vec<_>) = state
217+
.config
218+
.hosts
219+
.interfaces
220+
.iter()
221+
.map(|iface| {
222+
let (tx, rx) = mpsc::channel(128);
223+
((iface.network, tx), (iface, rx))
224+
})
225+
.unzip();
205226

206-
let (tx, rx) = mpsc::channel(128);
207227
let socket = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, ACTION_PORT)).await?;
208228
log::info!("Listening on {}", socket.local_addr()?);
209229
socket.set_broadcast(true)?;
210230

211-
tokio::try_join!(
212-
broadcast_chunks(&state, &socket, network.broadcast(), rx),
213-
broadcast_hint(&state, &socket, network.broadcast()),
214-
handle_requests(&state, &socket, tx),
215-
)?;
231+
let mut tasks = vec![handle_requests(&state, &socket, net_tx).boxed()];
232+
233+
for (iface, rx) in net_rx {
234+
tasks.push(broadcast_chunks(&state, &socket, iface, rx).boxed());
235+
tasks.push(broadcast_hint(&state, &socket, iface.network.broadcast()).boxed());
236+
}
237+
238+
futures::future::try_join_all(tasks).await?;
216239

217240
Ok(())
218241
}

pixie-shared/src/config.rs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::{Action, Bijection};
22
use alloc::{string::String, vec::Vec};
3+
use ipnet::Ipv4Net;
34
use macaddr::MacAddr6;
45
use serde::{Deserialize, Serialize};
56
use std::{
@@ -18,20 +19,26 @@ pub enum DhcpMode {
1819
Proxy(Ipv4Addr),
1920
}
2021

22+
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize, Clone)]
23+
pub struct InterfaceConfig {
24+
/// Listen on address
25+
pub network: Ipv4Net,
26+
/// DHCP server.
27+
pub dhcp: DhcpMode,
28+
/// Speed in bytes/second used to broadcast chunks.
29+
pub broadcast_speed: u32,
30+
}
31+
2132
/// Registered clients will always be assigned an IP in the form
2233
/// 10.{group_id}.{column_id}.{row_id}.
2334
/// Note that for this to work, the specified network interface must have an IP on the 10.0.0.0/8
2435
/// subnet; BEWARE that dnsmasq can be picky about the order of IP addresses.
2536
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize, Clone)]
2637
pub struct HostsConfig {
27-
/// Listen on address
28-
pub listen_on: Ipv4Addr,
29-
/// DHCP server.
30-
pub dhcp: DhcpMode,
38+
/// Interfaces to operate on.
39+
pub interfaces: Vec<InterfaceConfig>,
3140
/// Hosts file to use for DHCP hostnames.
3241
pub hostsfile: Option<PathBuf>,
33-
/// Speed in bytes/second used to broadcast chunks.
34-
pub broadcast_speed: u32,
3542
}
3643

3744
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]

run_test.sh

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ cleanup() {
2121
trap cleanup EXIT
2222

2323
cp -rv $1 $TEMPDIR
24+
cp test_config.yaml $TEMPDIR/storage/config.yaml
2425

2526
cat >$TEMPDIR/storage/registered.json <<EOF
2627
[
@@ -33,14 +34,30 @@ cat >$TEMPDIR/storage/registered.json <<EOF
3334
"curr_progress": null,
3435
"next_action": "shutdown",
3536
"image": "contestant"
37+
},
38+
{
39+
"mac": [82, 84, 0, 18, 52, 87],
40+
"group": 10,
41+
"row": 9,
42+
"col": 1,
43+
"curr_action": null,
44+
"curr_progress": null,
45+
"next_action": "shutdown",
46+
"image": "contestant"
3647
}
3748
]
3849
EOF
3950

4051
if ! ip link show br-pixie; then
4152
ip link add br-pixie type bridge
4253
ip link set dev br-pixie up
43-
ip addr add 10.0.0.1/8 brd 10.255.255.255 dev br-pixie
54+
ip addr add 10.0.0.1/16 dev br-pixie
55+
fi
56+
57+
if ! ip link show br-pixie1; then
58+
ip link add br-pixie1 type bridge
59+
ip link set dev br-pixie1 up
60+
ip addr add 10.10.0.1/16 dev br-pixie1
4461
fi
4562

4663
RUST_BACKTRACE=short RUST_LOG=debug RUST_LOG_STYLE=always LLVM_PROFILE_FILE=prof-out/pixie-server-%m-%p.profraw ./pixie-server/target/debug/pixie-server -s $TEMPDIR/storage &
@@ -67,7 +84,30 @@ run_qemu() {
6784
-nic bridge,mac=52:54:00:12:34:56,br=br-pixie,model=e1000
6885
}
6986

87+
run_qemu1() {
88+
OVMF=/usr/share/OVMF/OVMF_CODE_4M.fd
89+
if ! [ -e $OVMF ]; then
90+
OVMF=/usr/share/edk2/x64/OVMF_CODE.4m.fd
91+
fi
92+
FILE=prof-out/pixie-uefi-$RANDOM.profraw
93+
truncate -s 500M $FILE
94+
qemu-system-x86_64 \
95+
-nographic \
96+
-chardev stdio,id=char0,logfile=$1,signal=off \
97+
-serial chardev:char0 \
98+
-monitor none \
99+
-enable-kvm \
100+
-cpu host -smp cores=2 \
101+
-m 1G \
102+
-drive if=pflash,format=raw,file=$OVMF \
103+
-drive file=$TEMPDIR/disk1.img,if=none,id=nvm,format=raw \
104+
-drive file=$FILE,format=raw \
105+
-device nvme,serial=deadbeef,drive=nvm \
106+
-nic bridge,mac=52:54:00:12:34:57,br=br-pixie1,model=e1000
107+
}
108+
70109
truncate -s 8G $TEMPDIR/disk.img
110+
truncate -s 8G $TEMPDIR/disk1.img
71111
echo -e "label: gpt\n- 1GiB - -\n- 1GiB - -\n- - L -" | sfdisk $TEMPDIR/disk.img
72112
DEV=$(losetup --partscan --show --find $TEMPDIR/disk.img)
73113
mkswap ${DEV}p1
@@ -80,7 +120,7 @@ for PART in ${DEV}p{2..3}; do
80120
done
81121
losetup -d $DEV
82122

83-
curl 'http://localhost:8080/admin/curr_action/all/store'
123+
curl 'http://localhost:8080/admin/curr_action/52:54:00:12:34:56/store'
84124
run_qemu $TEMPDIR/store.log
85125

86126
# Check that we restore the original disk image.
@@ -90,6 +130,7 @@ truncate -s 8G $TEMPDIR/disk.img
90130

91131
curl 'http://localhost:8080/admin/curr_action/all/flash'
92132
run_qemu $TEMPDIR/flash-1.log
133+
run_qemu1 $TEMPDIR/flash1-1.log
93134

94135
DEV=$(losetup --partscan --show --find --read-only $TEMPDIR/disk.img)
95136
fsck -n ${DEV}p*
@@ -103,10 +144,23 @@ for PART in ${DEV}p{2..3}; do
103144
done
104145
losetup -d $DEV
105146

147+
DEV=$(losetup --partscan --show --find --read-only $TEMPDIR/disk1.img)
148+
fsck -n ${DEV}p*
149+
for PART in ${DEV}p{2..3}; do
150+
mount -o ro $PART $TEMPDIR/mnt
151+
if [ "$(md5sum $TEMPDIR/mnt/pixie-server | cut -f 1 -d ' ')" != "$(md5sum ./pixie-server/target/debug/pixie-server | cut -f 1 -d ' ')" ]; then
152+
echo "pixie-server does not contain the expected content"
153+
exit 1
154+
fi
155+
umount $TEMPDIR/mnt
156+
done
157+
losetup -d $DEV
158+
106159
# Check that we don't fetch any data if the disk contents have not changed.
107160

108161
curl 'http://localhost:8080/admin/curr_action/all/flash'
109162
run_qemu $TEMPDIR/flash-2.log
163+
run_qemu1 $TEMPDIR/flash1-2.log
110164

111165
DEV=$(losetup --partscan --show --find --read-only $TEMPDIR/disk.img)
112166
fsck -n ${DEV}p*
@@ -120,7 +174,24 @@ for PART in ${DEV}p{2..3}; do
120174
done
121175
losetup -d $DEV
122176

177+
DEV=$(losetup --partscan --show --find --read-only $TEMPDIR/disk1.img)
178+
fsck -n ${DEV}p*
179+
for PART in ${DEV}p{2..3}; do
180+
mount -o ro $PART $TEMPDIR/mnt
181+
if [ "$(md5sum $TEMPDIR/mnt/pixie-server | cut -f 1 -d ' ')" != "$(md5sum ./pixie-server/target/debug/pixie-server | cut -f 1 -d ' ')" ]; then
182+
echo "pixie-server does not contain the expected content"
183+
exit 1
184+
fi
185+
umount $TEMPDIR/mnt
186+
done
187+
losetup -d $DEV
188+
123189
if ! grep "Disk scanned; 0 chunks to fetch" $TEMPDIR/flash-2.log &>/dev/null; then
124190
echo "Data was re-fetched"
125191
exit 1
126192
fi
193+
194+
if ! grep "Disk scanned; 0 chunks to fetch" $TEMPDIR/flash1-2.log &>/dev/null; then
195+
echo "Data was re-fetched"
196+
exit 1
197+
fi

test_config.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
hosts:
2+
interfaces:
3+
- network: 10.0.0.1/16
4+
dhcp: !static [10.0.100.1, 10.0.200.200]
5+
broadcast_speed: 52428800
6+
- network: 10.10.0.1/16
7+
dhcp: !static [10.10.100.1, 10.10.200.200]
8+
broadcast_speed: 52428800
9+
hostsfile: /etc/hosts
10+
http:
11+
listen_on: 0.0.0.0:8080
12+
groups:
13+
- [room0, 0]
14+
- [room10, 10]
15+
images:
16+
- contestant
17+
- worker

0 commit comments

Comments
 (0)