Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ jobs:
scripts/network/test-bridge-native.ps1
scripts/network/test-npcap-frames.ps1
scripts/network/test-npcap-segmentation.ps1
scripts/network/test-launcher-bridge.ps1
scripts/network/test-launcher-bridge-input.ps1

- name: Test bridge lab safety in Windows PowerShell
shell: powershell
Expand All @@ -96,6 +98,8 @@ jobs:
scripts/network/test-bridge-native.ps1
scripts/network/test-npcap-frames.ps1
scripts/network/test-npcap-segmentation.ps1
scripts/network/test-launcher-bridge.ps1
scripts/network/test-launcher-bridge-input.ps1

guest-contract:
name: Guest contract
Expand Down
4 changes: 4 additions & 0 deletions app/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ module github.com/omacom/try-omarchy-windows/app
go 1.27

require github.com/klauspost/compress v1.19.2

require github.com/omacom/try-omarchy-windows/networkpayload v0.0.0

replace github.com/omacom/try-omarchy-windows/networkpayload => ../scripts/network
97 changes: 97 additions & 0 deletions app/lan_bridge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package main

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net"
"os"
"regexp"
"strings"
)

// bridgePlan is an explicit, installation-local lab opt-in. It is not inferred
// from an alias, current default route, or an installed VPN's TAP.
type bridgePlan struct {
Version int `json:"version"`
WiredGuid string `json:"wiredGuid"`
WiredPnp string `json:"wiredPnp"`
TapGuid string `json:"tapGuid"`
TapPnp string `json:"tapPnp"`
LANMac string `json:"lanMac"`
PrivateMac string `json:"privateMac"`
DriverDirectory string `json:"driverDirectory"`
ProbeName string `json:"probeName"`
ProbeAddress string `json:"probeAddress"`
ProbePort int `json:"probePort"`
DisposableLab bool `json:"disposableLab"`
LocalConsole bool `json:"localConsole"`
DedicatedTap bool `json:"dedicatedTap"`
GuestNetworkPrepared bool `json:"guestNetworkPrepared"`
}

var bridgeGUID = regexp.MustCompile(`(?i)^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)

func (p bridgePlan) validate() error {
if p.Version != 1 || !p.DisposableLab || !p.LocalConsole || !p.DedicatedTap || !p.GuestNetworkPrepared {
return fmt.Errorf("bridge requires an explicit disposable wired lab, local console, dedicated TAP and prepared guest routes")
}
if !bridgeGUID.MatchString(p.WiredGuid) || !bridgeGUID.MatchString(p.TapGuid) || strings.EqualFold(p.WiredGuid, p.TapGuid) {
return fmt.Errorf("select distinct exact wired and TAP GUIDs")
}
for _, v := range []string{p.WiredPnp, p.TapPnp, p.DriverDirectory, p.ProbeName} {
if v == "" || len(v) > 1024 || strings.ContainsAny(v, "\x00\r\n") {
return fmt.Errorf("bridge identity or probe is invalid")
}
}
for _, v := range []string{p.LANMac, p.PrivateMac} {
mac, e := net.ParseMAC(v)
if e != nil || len(mac) != 6 || len(v) != 17 || !strings.Contains(v, ":") || mac[0]&3 != 2 {
return fmt.Errorf("bridge MACs must be locally administered unicast Ethernet addresses")
}
}
if strings.EqualFold(p.LANMac, p.PrivateMac) {
return fmt.Errorf("LAN and private MACs must differ")
}
ip := net.ParseIP(p.ProbeAddress)
if ip == nil || ip.To4() == nil || !ip.IsGlobalUnicast() || ip.IsLoopback() || p.ProbePort < 1 || p.ProbePort > 65535 {
return fmt.Errorf("bridge requires a separate IPv4 wired TCP probe")
}
return nil
}
func loadBridgePlan(path string) (*bridgePlan, error) {
f, e := os.Open(path)
if e != nil {
return nil, e
}
defer f.Close()
data, e := io.ReadAll(io.LimitReader(f, 65537))
if e != nil {
return nil, e
}
if len(data) > 65536 {
return nil, fmt.Errorf("bridge plan is too large")
}
d := json.NewDecoder(bytes.NewReader(bytes.TrimPrefix(data, []byte{239, 187, 191})))
d.DisallowUnknownFields()
var p bridgePlan
if e = d.Decode(&p); e != nil {
return nil, e
}
if e = d.Decode(&struct{}{}); e != io.EOF {
return nil, fmt.Errorf("bridge plan has trailing data")
}
if e = p.validate(); e != nil {
return nil, e
}
return &p, nil
}
func bridgeNetworkArgs(p *bridgePlan, name string, forwards []portForward) []string {
if p == nil {
return []string{"-device", "virtio-net-pci,netdev=n0", "-netdev", netdevArg(forwards)}
}
// Place LAN first, but keep the private service network and every existing
// forward. The prepared guest disables private default routes and DNS.
return []string{"-device", "virtio-net-pci,netdev=lan0,mac=" + strings.ToLower(p.LANMac), "-netdev", "tap,id=lan0,ifname=" + qemuOptionValue(name), "-device", "virtio-net-pci,netdev=n0,mac=" + strings.ToLower(p.PrivateMac), "-netdev", netdevArg(forwards)}
}
64 changes: 64 additions & 0 deletions app/lan_bridge_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package main

import (
"encoding/json"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
)

func validBridgePlan() bridgePlan {
return bridgePlan{Version: 1, WiredGuid: "fbcf0905-41e4-498a-b086-7f7771f57184", WiredPnp: `PCI\OWNED`, TapGuid: "14349899-ada4-4135-b6ae-6acd66845ee2", TapPnp: `ROOT\NET\0000`, LANMac: "52:54:00:16:66:01", PrivateMac: "52:54:00:16:66:02", DriverDirectory: `C:\BridgeLab\driver`, ProbeName: "bridge-peer.test", ProbeAddress: "192.0.2.1", ProbePort: 8080, DisposableLab: true, LocalConsole: true, DedicatedTap: true, GuestNetworkPrepared: true}
}
func TestBridgePlanRejectsUnsafeSelection(t *testing.T) {
cases := map[string]func(*bridgePlan){"version": func(p *bridgePlan) { p.Version = 2 }, "remote": func(p *bridgePlan) { p.LocalConsole = false }, "physical": func(p *bridgePlan) { p.DisposableLab = false }, "borrowed": func(p *bridgePlan) { p.DedicatedTap = false }, "unprepared": func(p *bridgePlan) { p.GuestNetworkPrepared = false }, "same-adapter": func(p *bridgePlan) { p.TapGuid = p.WiredGuid }, "alias": func(p *bridgePlan) { p.WiredGuid = "Ethernet" }, "missing-pnp": func(p *bridgePlan) { p.TapPnp = "" }, "multicast": func(p *bridgePlan) { p.LANMac = "53:54:00:16:66:01" }, "same-mac": func(p *bridgePlan) { p.PrivateMac = p.LANMac }, "global-mac": func(p *bridgePlan) { p.LANMac = "00:54:00:16:66:01" }, "loopback-probe": func(p *bridgePlan) { p.ProbeAddress = "127.0.0.1" }, "ipv6-probe": func(p *bridgePlan) { p.ProbeAddress = "::1" }, "port": func(p *bridgePlan) { p.ProbePort = 0 }}
if e := validBridgePlan().validate(); e != nil {
t.Fatal(e)
}
for name, change := range cases {
t.Run(name, func(t *testing.T) {
p := validBridgePlan()
change(&p)
if p.validate() == nil {
t.Fatal("accepted unsafe plan")
}
})
}
}
func TestBridgePlanFileValidation(t *testing.T) {
p := validBridgePlan()
data, _ := json.Marshal(p)
path := filepath.Join(t.TempDir(), "plan.json")
for _, bad := range []string{string(data) + " {}", strings.Replace(string(data), `"version":1`, `"unknown":1`, 1), strings.Repeat("x", 65537)} {
os.WriteFile(path, []byte(bad), 0600)
if _, e := loadBridgePlan(path); e == nil {
t.Fatal("accepted invalid file")
}
}
os.WriteFile(path, append([]byte{239, 187, 191}, data...), 0600)
got, e := loadBridgePlan(path)
if e != nil || !reflect.DeepEqual(*got, p) {
t.Fatalf("BOM plan: %v %v", got, e)
}
}
func TestBridgeKeepsPrivateServicesAndForwards(t *testing.T) {
var forwards forwardList
if e := forwards.Set("tcp:18092:8082"); e != nil {
t.Fatal(e)
}
nat := bridgeNetworkArgs(nil, "", forwards)
want := []string{"-device", "virtio-net-pci,netdev=n0", "-netdev", netdevArg(forwards)}
if !reflect.DeepEqual(nat, want) {
t.Fatal(nat)
}
p := validBridgePlan()
args := bridgeNetworkArgs(&p, "Owned, TAP", forwards)
if args[1] != "virtio-net-pci,netdev=lan0,mac="+p.LANMac || args[3] != "tap,id=lan0,ifname=Owned,, TAP" || args[5] != "virtio-net-pci,netdev=n0,mac="+p.PrivateMac || args[7] != nat[3] {
t.Fatal(args)
}
if !strings.Contains(args[7], "hostfwd=tcp:127.0.0.1:18092-:8082") {
t.Fatal("forward escaped private NAT", args)
}
}
Loading
Loading