Skip to content

Commit 44e3e10

Browse files
authored
Merge pull request #100 from sysprog21/macos-network
Add macOS user-mode networking support
2 parents 6c8649a + 0b9320b commit 44e3e10

File tree

15 files changed

+1431
-79
lines changed

15 files changed

+1431
-79
lines changed

.ci/detect-vmnet-network.sh

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
#!/usr/bin/env bash
2+
3+
# Detect vmnet shared network parameters (gateway IP, prefix)
4+
# Outputs: "<guest_ip> <gateway_ip> <prefix_len>"
5+
6+
set -euo pipefail
7+
8+
timeout="${VMNET_DETECT_TIMEOUT:-30}"
9+
guest_octet="${VMNET_GUEST_HOST_OCTET:-10}"
10+
guest_ip_override="${VMNET_GUEST_IP_OVERRIDE:-}"
11+
target_bridge="${VMNET_BRIDGE:-}"
12+
13+
convert_netmask_to_prefix() {
14+
local mask="$1"
15+
case "${mask}" in
16+
0xffffff00) echo 24 ;;
17+
0xfffffe00) echo 23 ;;
18+
0xfffffc00) echo 22 ;;
19+
0xfffff800) echo 21 ;;
20+
0xfffff000) echo 20 ;;
21+
0xffffe000) echo 19 ;;
22+
0xffffc000) echo 18 ;;
23+
0xffff8000) echo 17 ;;
24+
0xffff0000) echo 16 ;;
25+
0xfffe0000) echo 15 ;;
26+
0xfffc0000) echo 14 ;;
27+
0xfff80000) echo 13 ;;
28+
0xfff00000) echo 12 ;;
29+
0xffe00000) echo 11 ;;
30+
0xffc00000) echo 10 ;;
31+
0xff800000) echo 9 ;;
32+
0xff000000) echo 8 ;;
33+
*) echo 24 ;;
34+
esac
35+
}
36+
37+
detect_bridge() {
38+
local candidates=""
39+
if [[ -n "${target_bridge}" ]]; then
40+
candidates="${target_bridge}"
41+
else
42+
candidates="$(ifconfig -l)"
43+
fi
44+
45+
local candidate
46+
for candidate in ${candidates}; do
47+
if [[ -n "${target_bridge}" ]] || [[ "${candidate}" =~ ^bridge[0-9]+$ ]]; then
48+
local inet_line
49+
inet_line="$(ifconfig "${candidate}" 2>/dev/null | awk '/inet /{print $0; exit}')"
50+
if [[ -n "${inet_line}" ]]; then
51+
echo "${candidate}|${inet_line}"
52+
return 0
53+
fi
54+
fi
55+
done
56+
57+
return 1
58+
}
59+
60+
while (( timeout > 0 )); do
61+
if bridge_info="$(detect_bridge)"; then
62+
bridge_name="${bridge_info%%|*}"
63+
inet_line="${bridge_info#*|}"
64+
65+
gateway="$(awk '{print $2}' <<<"${inet_line}")"
66+
netmask="$(awk '{print $4}' <<<"${inet_line}")"
67+
prefix="$(convert_netmask_to_prefix "${netmask}")"
68+
69+
IFS=. read -r o1 o2 o3 o4 <<<"${gateway}"
70+
if [[ -z "${o1:-}" || -z "${o2:-}" || -z "${o3:-}" ]]; then
71+
echo "Error: Unexpected gateway format: ${gateway}" >&2
72+
exit 1
73+
fi
74+
75+
guest_ip="${guest_ip_override}"
76+
if [[ -z "${guest_ip}" ]]; then
77+
guest_ip="${o1}.${o2}.${o3}.${guest_octet}"
78+
fi
79+
80+
printf "%s %s %s\n" "${guest_ip}" "${gateway}" "${prefix}"
81+
exit 0
82+
fi
83+
84+
sleep 1
85+
timeout=$((timeout - 1))
86+
done
87+
88+
echo "Error: vmnet gateway not detected within timeout" >&2
89+
exit 1

.ci/test-netdev.sh

Lines changed: 97 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
# Source common functions and settings
44
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5+
export SCRIPT_DIR
56
source "${SCRIPT_DIR}/common.sh"
67

78
# Override timeout for netdev tests
@@ -21,44 +22,118 @@ esac
2122
# Clean up any existing semu processes before starting tests
2223
cleanup
2324

24-
# Test network device functionality
25-
TEST_NETDEV() {
26-
local NETDEV="$1"
27-
local CMD_PREFIX=""
25+
# Platform detection
26+
UNAME_S=$(uname -s)
27+
28+
# Check if running on macOS
29+
if [[ ${UNAME_S} == "Darwin" ]]; then
30+
IS_MACOS=1
31+
else
32+
IS_MACOS=0
33+
fi
2834

29-
if [ "$NETDEV" = "tap" ]; then
30-
CMD_PREFIX="sudo "
35+
# Network device to test (can be overridden by NETDEV environment variable)
36+
NETDEV=${NETDEV:-""}
37+
38+
# Check for root privileges based on network device
39+
REQUIRES_SUDO=0
40+
if [[ ${IS_MACOS} -eq 1 && "${NETDEV}" == "vmnet" ]]; then
41+
REQUIRES_SUDO=1
42+
SUDO_REASON="vmnet.framework requires root privileges"
43+
elif [[ ${IS_MACOS} -eq 0 && "${NETDEV}" == "tap" ]]; then
44+
REQUIRES_SUDO=1
45+
SUDO_REASON="TAP device requires root privileges"
46+
fi
47+
48+
# Check for root privileges if required
49+
if [[ ${REQUIRES_SUDO} -eq 1 ]]; then
50+
if [[ $EUID -ne 0 ]]; then
51+
echo "Error: This script must be run with sudo for ${NETDEV} mode"
52+
echo "Reason: ${SUDO_REASON}"
53+
echo ""
54+
echo "Usage: sudo $0"
55+
echo "Or use NETDEV=user for no-sudo mode (macOS/Linux)"
56+
exit 1
3157
fi
58+
fi
59+
60+
# Test network device functionality
61+
TEST_NETDEV() {
62+
local NETDEV=$1
3263

3364
ASSERT expect <<DONE
3465
set timeout ${TIMEOUT}
35-
spawn ${CMD_PREFIX}make check NETDEV=${NETDEV}
36-
expect "buildroot login:" { send "root\n" } timeout { exit 1 }
37-
expect "# " { send "uname -a\n" } timeout { exit 2 }
66+
spawn make check NETDEV=${NETDEV}
67+
expect "buildroot login:" { send "root\\n" } timeout { exit 1 }
68+
expect "# " { send "uname -a\\n" } timeout { exit 2 }
69+
3870
if { "$NETDEV" == "tap" } {
39-
exec sudo ip addr add 192.168.10.1/24 dev tap0
40-
exec sudo ip link set tap0 up
41-
expect "riscv32 GNU/Linux" { send "ip l set eth0 up\n" } timeout { exit 3 }
42-
expect "# " { send "ip a add 192.168.10.2/24 dev eth0\n" }
43-
expect "# " { send "ping -c 3 192.168.10.1\n" }
71+
exec ip addr add 192.168.10.1/24 dev tap0
72+
exec ip link set tap0 up
73+
expect "riscv32 GNU/Linux" { send "ip l set eth0 up\\n" } timeout { exit 3 }
74+
expect "# " { send "ip a add 192.168.10.2/24 dev eth0\\n" }
75+
expect "# " { send "ping -c 3 192.168.10.1\\n" }
4476
expect "3 packets transmitted, 3 packets received, 0% packet loss" { } timeout { exit 4 }
4577
} elseif { "$NETDEV" == "user" } {
46-
expect "riscv32 GNU/Linux" { send "ip addr add 10.0.2.15/24 dev eth0\n" } timeout { exit 3 }
47-
expect "# " { send "ip link set eth0 up\n"}
48-
expect "# " { send "ip route add default via 10.0.2.2\n"}
49-
expect "# " { send "ping -c 3 10.0.2.2\n" }
78+
expect "riscv32 GNU/Linux" { send "ip addr add 10.0.2.15/24 dev eth0\\n" } timeout { exit 3 }
79+
expect "# " { send "ip link set eth0 up\\n"}
80+
expect "# " { send "ip route add default via 10.0.2.2\\n"}
81+
expect "# " { send "ping -c 3 10.0.2.2\\n" }
82+
expect "3 packets transmitted, 3 packets received, 0% packet loss" { } timeout { exit 4 }
83+
} elseif { "$NETDEV" == "vmnet" } {
84+
# vmnet (macOS): detect host-provided gateway and configure statically
85+
set vmnet_info [exec \$env(SCRIPT_DIR)/detect-vmnet-network.sh]
86+
set vmnet_guest_ip [lindex \$vmnet_info 0]
87+
set vmnet_gateway [lindex \$vmnet_info 1]
88+
set vmnet_prefix [lindex \$vmnet_info 2]
89+
90+
expect "riscv32 GNU/Linux" { send "ip link set eth0 up\\n" } timeout { exit 3 }
91+
expect "# " { send "ip addr flush dev eth0\\n" }
92+
expect "# " { send "ip addr add \$vmnet_guest_ip/\$vmnet_prefix dev eth0\\n" }
93+
expect "# " { send "ip route replace default via \$vmnet_gateway\\n" }
94+
expect "# " { send "for i in 1 2 3 4 5; do ping -c 1 -W 3 \$vmnet_gateway && break; sleep 1; done\\n" }
95+
expect "# " { send "ping -c 3 \$vmnet_gateway\\n" }
5096
expect "3 packets transmitted, 3 packets received, 0% packet loss" { } timeout { exit 4 }
5197
}
5298
DONE
5399
}
54100

55-
# Network devices to test
56-
NETWORK_DEVICES=(tap user)
101+
# Determine network devices to test based on platform
102+
if [[ -n "${NETDEV}" ]]; then
103+
# NETDEV environment variable specified - test only that device
104+
NETWORK_DEVICES=(${NETDEV})
105+
elif [[ ${IS_MACOS} -eq 1 ]]; then
106+
# macOS: test both user (no sudo) and vmnet (requires sudo)
107+
# Default to user if not running as root
108+
if [[ $EUID -eq 0 ]]; then
109+
NETWORK_DEVICES=(user vmnet)
110+
else
111+
NETWORK_DEVICES=(user)
112+
echo "Note: Running without sudo, testing user mode only"
113+
echo "Run with 'sudo' to test vmnet mode"
114+
fi
115+
else
116+
# Linux: test tap (requires sudo) and user (no sudo)
117+
if [[ $EUID -eq 0 ]]; then
118+
NETWORK_DEVICES=(tap user)
119+
else
120+
NETWORK_DEVICES=(user)
121+
echo "Note: Running without sudo, testing user mode only"
122+
echo "Run with 'sudo' to test tap mode"
123+
fi
124+
fi
125+
126+
echo "Platform: ${UNAME_S}"
127+
echo "Network devices to test: ${NETWORK_DEVICES[@]}"
57128

58129
for NETDEV in "${NETWORK_DEVICES[@]}"; do
59130
cleanup
60-
echo "Test network device: $NETDEV"
61-
TEST_NETDEV "$NETDEV"
131+
echo ""
132+
echo "========================================="
133+
echo "Testing network device: $NETDEV"
134+
echo "========================================="
135+
TEST_NETDEV $NETDEV
136+
echo "$NETDEV test passed"
62137
done
63138

64139
ret="$?"

.github/workflows/main.yml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ jobs:
6666
shell: bash
6767
timeout-minutes: 10
6868
- name: netdev test
69-
run: .ci/test-netdev.sh
69+
run: sudo .ci/test-netdev.sh
7070
shell: bash
7171
timeout-minutes: 10
7272

@@ -104,6 +104,20 @@ jobs:
104104
run: .ci/autorun.sh
105105
shell: bash
106106
timeout-minutes: 15
107+
- name: netdev test
108+
# NOTE: vmnet requires sudo privileges which may not be available in GitHub Actions
109+
# This test is conditional and will be skipped if sudo is not available
110+
run: |
111+
if sudo -n true 2>/dev/null; then
112+
echo "sudo available, running netdev test"
113+
sudo .ci/test-netdev.sh
114+
else
115+
echo "Skipping netdev test: sudo not available in GitHub Actions runner"
116+
echo "vmnet.framework requires root privileges or com.apple.vm.networking entitlement"
117+
fi
118+
shell: bash
119+
timeout-minutes: 20
120+
if: ${{ success() }}
107121

108122
coding_style:
109123
runs-on: ubuntu-24.04
@@ -112,6 +126,8 @@ jobs:
112126
pull-requests: write
113127
steps:
114128
- uses: actions/checkout@v4
129+
with:
130+
submodules: recursive
115131
- name: cache apt packages
116132
uses: actions/cache@v4
117133
with:

.gitmodules

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
shallow = true
55
[submodule "minislirp"]
66
path = minislirp
7-
url = https://github.com/edubart/minislirp
7+
url = https://github.com/sysprog21/minislirp
88
shallow = true
99
[submodule "portaudio"]
1010
path = portaudio

Makefile

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,31 @@ endif
5353
NETDEV ?= tap
5454
# virtio-net
5555
ENABLE_VIRTIONET ?= 1
56-
ifneq ($(UNAME_S),Linux)
56+
ifeq ($(UNAME_S),Darwin)
57+
# macOS: auto-select backend when using default TAP setting
58+
ifeq ($(NETDEV),tap)
59+
NETDEV :=
60+
endif
61+
else ifneq ($(UNAME_S),Linux)
62+
# Other platforms: disable virtio-net
5763
ENABLE_VIRTIONET := 0
5864
endif
65+
5966
$(call set-feature, VIRTIONET)
6067
ifeq ($(call has, VIRTIONET), 1)
6168
OBJS_EXTRA += virtio-net.o
6269
OBJS_EXTRA += netdev.o
63-
OBJS_EXTRA += slirp.o
70+
71+
ifeq ($(UNAME_S),Darwin)
72+
# macOS: support both vmnet and user (slirp) backends
73+
OBJS_EXTRA += netdev-vmnet.o
74+
OBJS_EXTRA += slirp.o
75+
CFLAGS += -fblocks
76+
LDFLAGS += -framework vmnet
77+
else
78+
# Linux: use tap/slirp backends
79+
OBJS_EXTRA += slirp.o
80+
endif
6481
endif
6582

6683
# virtio-snd
@@ -168,10 +185,16 @@ ifeq ($(call has, VIRTIONET), 1)
168185
MINISLIRP_DIR := minislirp
169186
MINISLIRP_LIB := minislirp/src/libslirp.a
170187
LDFLAGS += $(MINISLIRP_LIB)
188+
# macOS: workaround for swab redefinition and add resolv library
189+
MINISLIRP_CFLAGS :=
190+
ifeq ($(UNAME_S),Darwin)
191+
MINISLIRP_CFLAGS := MYCFLAGS="-D_DARWIN_C_SOURCE"
192+
LDFLAGS += -lresolv
193+
endif
171194
$(MINISLIRP_DIR)/src/Makefile:
172195
git submodule update --init $(MINISLIRP_DIR)
173196
$(MINISLIRP_LIB): $(MINISLIRP_DIR)/src/Makefile
174-
$(MAKE) -C $(dir $<)
197+
$(MAKE) -C $(dir $<) $(MINISLIRP_CFLAGS)
175198
$(OBJS): $(MINISLIRP_LIB)
176199
endif
177200

@@ -243,7 +266,7 @@ $(SHARED_DIRECTORY):
243266

244267
check: $(BIN) minimal.dtb $(KERNEL_DATA) $(INITRD_DATA) $(DISKIMG_FILE) $(SHARED_DIRECTORY)
245268
@$(call notice, Ready to launch Linux kernel. Please be patient.)
246-
$(Q)./$(BIN) -k $(KERNEL_DATA) -c $(SMP) -b minimal.dtb -i $(INITRD_DATA) -n $(NETDEV) $(OPTS)
269+
$(Q)./$(BIN) -k $(KERNEL_DATA) -c $(SMP) -b minimal.dtb -i $(INITRD_DATA) $(if $(NETDEV),-n $(NETDEV)) $(OPTS)
247270

248271
build-image:
249272
scripts/build-image.sh

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ You can exit the emulator using: \<Ctrl-a x\>. (press Ctrl+A, leave it, afterwar
8686
* `disk-image` is optional, as it specifies the path of a disk image in ext4 file system for the virtio-blk device.
8787
* `shared-directory` is optional, as it specifies the path of a directory on the host that will be shared with the guest operating system through virtio-fs, enabling file access from the guest via a virtual filesystem mount.
8888

89+
For detailed networking guidance, see [`docs/networking.md`](docs/networking.md).
90+
8991
## Mount and unmount a directory in semu
9092

9193
To mount the directory in semu:

common.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ static inline int ilog2(int x)
1717
return 31 - __builtin_clz(x | 1);
1818
}
1919

20+
#define MIN(a, b) (((a) < (b)) ? (a) : (b))
21+
2022
/* Range check
2123
* For any variable range checking:
2224
* if (x >= minx && x <= maxx) ...

0 commit comments

Comments
 (0)