diff --git a/network/daemon.go b/network/daemon.go new file mode 100644 index 0000000..e3864fc --- /dev/null +++ b/network/daemon.go @@ -0,0 +1,200 @@ +package network + +import( + //Builtin + "errors" + "fmt" + //"math" + "net" + "os" + //"os/exec" + "strings" + + //Internal + + //External + "github.com/milosgajdos83/tenus" + "github.com/op/go-logging" +) + + +func BridgeInit(log *logging.Logger) (*HostNetwork, error) { + htn := new(HostNetwork) + + if os.Getpid() == 1 { + panic(errors.New("Cannot use netinit from child.")) + } + + // Fetch the bridge interface by ifname + brL, err := net.InterfaceByName(ozDefaultInterfaceBridge) + if err != nil { + log.Info("Bridge not found, attempting to create a new one") + + _, err = createNewBridge(log) + if err != nil { + return nil, fmt.Errorf("Unable to create bridge %+v", err) + } + + // Load the new interface + brL, _ = net.InterfaceByName(ozDefaultInterfaceBridge) + } else { + log.Info("Bridge already exists attempting to reuse it") + } + + // Lookup the bridge ip addresses + addrs, _ := brL.Addrs() + if len(addrs) == 0 { + return nil, errors.New("Host bridge does not have an IP address assigned") + } + + // Try to build the network config from the bridge's address + addrIndex := -1 + for i, addr := range addrs { + bIP, brIP, _ := net.ParseCIDR(addr.String()) + + // Discard IPv6 (TODO...) + if bIP.To4() != nil { + addrIndex = i + + bMask := []byte(brIP.Mask) + + htn.netmask = net.IPv4(bMask[0], bMask[1], bMask[2], bMask[3]) + htn.gateway = net.ParseIP(strings.Split(addr.String(), "/")[0]) + htn.class = strings.Split(addr.String(), "/")[1] + htn.broadcast = net_getbroadcast(bIP, brIP.Mask) + + htn.min = inet_aton(bIP) + htn.min++ + + htn.max = inet_aton(htn.broadcast) + htn.max-- + + break + } + + } + + if addrIndex < 0 { + return nil, errors.New("Could not find IPv4 for bridge interface") + } + + return htn, nil +} + + +// Create a new bridge on the host +// Assign it an unused range +// And bring the interface up +func createNewBridge(log *logging.Logger) (tenus.Bridger, error) { + if os.Getpid() == 1 { + panic(errors.New("Cannot use netinit from child.")) + } + + // Create the bridge + br, err := tenus.NewBridgeWithName(ozDefaultInterfaceBridge) + if err != nil { + return nil, err + } + + // Lookup an empty ip range + brIp, brIpNet, err := findEmptyRange(log) + if err != nil { + return nil, errors.New("Could not find an ip range to assign to the bridge") + } + + // Setup the bridge's address + if err := br.SetLinkIp(brIp, brIpNet); err != nil { + return nil, err + } + + // Bridge the interface up + if err = br.SetLinkUp(); err != nil { + return nil, fmt.Errorf("Unable to bring bridge '%+v' up: %+v", ozDefaultInterfaceBridge, err) + } + + return br, nil + +} + + +// Look at all the assigned IP address and try to find an available range +// Returns a ip range in the CIDR form if found or an empty string +func findEmptyRange(log *logging.Logger) (net.IP, *net.IPNet, error) { + type localNet struct { + min uint64 + max uint64 + } + + var ( + localNets []localNet + availableRange string + ) + + // List all the available interfaces and their addresses + // and calulate their network's min and max values + ifs, _ := net.Interfaces() + for _, netif := range ifs { + // Disable loopback and our bridge + if netif.Name == ozDefaultInterfaceBridge || + strings.HasPrefix(netif.Name, "lo") { + continue + } + + // Go through each address on the interface + addrs, _ := netif.Addrs() + for _, addr := range addrs { + bIP, brIP, _ := net.ParseCIDR(addr.String()) + + // Discard non IPv4 addresses + if bIP.To4() != nil { + min := inet_aton(brIP.IP) + min++ + + max := inet_aton(net_getbroadcast(bIP, brIP.Mask)) + max-- + + localNets = append(localNets, localNet{min: min, max: max}) + } + } + } + + // Go through the list of private network ranges and + // look for one in which we cannot find a local network + for _, ipRange := range privateNetworkRanges { + bIP, brIP, err := net.ParseCIDR(ipRange) + if err != nil { + continue + } + + bMin := inet_aton(bIP) + bMax := inet_aton(net_getbroadcast(bIP, brIP.Mask)) + + alreadyUsed := false + for _, add := range localNets { + if add.min >= bMin && add.min < bMax && + add.max > bMin && add.max <= bMax { + alreadyUsed = true + break + + } + } + + // If the range is available, grab a small slice + if alreadyUsed == false { + bRange := bMax - bMin + if bRange > 0xFF { + bMin = bMax - 0xFE + } + + availableRange = inet_ntoa(bMin).String() + "/24" + + log.Info("Found available range: %+v", availableRange) + + return net.ParseCIDR(availableRange) + } + + } + + return nil, nil, errors.New("Could not find an available range") + +} diff --git a/network/network.go b/network/network.go new file mode 100644 index 0000000..dc56b6f --- /dev/null +++ b/network/network.go @@ -0,0 +1,109 @@ +package network + +import ( + //Builtin + "net" + "strings" + "strconv" +) + +const ( + ozDefaultInterfaceBridgeBase = "oz" + ozDefaultInterfaceBridge = ozDefaultInterfaceBridgeBase + "0" + ozDefaultInterfacePrefix = "veth" + ozDefaultInterfaceInternal = "eth0" + ozMaxRandTries = 3 +) + +type HostNetwork struct { + // Host bridge IP address + hostip net.IP + // Gateway ip (bridge ip) + gateway net.IP + // Bridge netmask + netmask net.IP + // Broadcast ip + broadcast net.IP + // IP class (ie: /24) + class string + // Minimum longip available ip + min uint64 + // Maximum longip available ip + max uint64 + +} + +type SandboxNetwork struct { + // Name of the veth in the host + vethHost string + // Temporary name of the guest' veth in the host + vethGuest string + // Guest ip address + ip string + + host *HostNetwork +} + +var privateNetworkRanges []string + +func init() { + privateNetworkRanges = []string{ + // RFC1918 Private ranges + "10.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16", + // Documentation / Testnet 2 + "192.51.100.0/24", + // Documentation/ Testnet + "192.0.2.0/24", + // Inter-network communication + "192.18.0.0/15", + // Documentation / Testnet 3 + "203.0.113.0/24", + // Carrier grade NAT + "100.64.0.0/10", + } +} + + +// Convert longip to net.IP +func inet_ntoa(ipnr uint64) net.IP { + var bytes [4]byte + bytes[0] = byte(ipnr & 0xFF) + bytes[1] = byte((ipnr >> 8) & 0xFF) + bytes[2] = byte((ipnr >> 16) & 0xFF) + bytes[3] = byte((ipnr >> 24) & 0xFF) + + return net.IPv4(bytes[3], bytes[2], bytes[1], bytes[0]) +} + +// Convert net.IP to longip +func inet_aton(ipnr net.IP) uint64 { + bits := strings.Split(ipnr.String(), ".") + + b0, _ := strconv.Atoi(bits[0]) + b1, _ := strconv.Atoi(bits[1]) + b2, _ := strconv.Atoi(bits[2]) + b3, _ := strconv.Atoi(bits[3]) + + var sum uint64 + + sum += uint64(b0) << 24 + sum += uint64(b1) << 16 + sum += uint64(b2) << 8 + sum += uint64(b3) + + return sum +} + +func net_getbroadcast(bIP net.IP, ipMask net.IPMask) net.IP { + bMask := []byte(ipMask) + byteIP := bIP.To4() + + return net.IPv4( + byteIP[0]|(bMask[0]^0xFF), + byteIP[1]|(bMask[1]^0xFF), + byteIP[2]|(bMask[2]^0xFF), + byteIP[3]|(bMask[3]^0xFF)) + +} diff --git a/network/ozinit.go b/network/ozinit.go new file mode 100644 index 0000000..79e0284 --- /dev/null +++ b/network/ozinit.go @@ -0,0 +1,216 @@ +package network + +import ( + //Builtin + "errors" + "fmt" + //"math" + "math/rand" + "net" + "os" + "time" + + //External + "github.com/j-keck/arping" + "github.com/milosgajdos83/tenus" + "github.com/op/go-logging" +) + +func NetInit(log *logging.Logger, htn *HostNetwork, childPid int) (*SandboxNetwork, error) { + if os.Getpid() == 1 { + panic(errors.New("Cannot use netSetup from child.")) + } + + stn := new(SandboxNetwork) + + stn.vethHost = tenus.MakeNetInterfaceName(ozDefaultInterfacePrefix) + stn.vethGuest = stn.vethHost + "1" + + // Seed random number generator (poorly but we're not doing crypto) + rand.Seed(time.Now().Unix() ^ int64((os.Getpid() + childPid))) + + log.Info("Configuring host veth pair: %s", stn.vethHost) + + // Fetch the bridge from the ifname + br, err := tenus.BridgeFromName(ozDefaultInterfaceBridge) + if err != nil { + return nil, fmt.Errorf("Unable to attach to bridge interface %, %s.", ozDefaultInterfaceBridge, err) + } + + // Create the veth pair + veth, err := tenus.NewVethPairWithOptions(stn.vethHost, tenus.VethOptions{PeerName: stn.vethGuest}) + if err != nil { + return nil, fmt.Errorf("Unable to create veth pair %s, %s.", stn.vethHost, err) + } + + // Fetch the newly created hostside veth + vethIf, err := net.InterfaceByName(stn.vethHost) + if err != nil { + return nil, fmt.Errorf("Unable to fetch veth pair %s, %s.", stn.vethHost, err) + } + + // Add the host side veth to the bridge + if err = br.AddSlaveIfc(vethIf); err != nil { + return nil, fmt.Errorf("Unable to add veth pair %s to bridge, %s.", stn.vethHost, err) + } + + // Bring the host side veth interface up + if err = veth.SetLinkUp(); err != nil { + return nil, fmt.Errorf("Unable to bring veth pair %s up, %s.", stn.vethHost, err) + } + + // Assign the veth path to the namespace + pid := childPid + if err := veth.SetPeerLinkNsPid(pid); err != nil { + return nil, fmt.Errorf("Unable to add veth pair %s to namespace, %s.", stn.vethHost, err) + } + + // Allocate a new IP address + stn.ip = getFreshIP(htn.min, htn.max, log) + if stn.ip == "" { + return nil, errors.New("Unable to acquire random IP") + } + + // Parse the ip/class into the the appropriate formats + vethGuestIp, vethGuestIpNet, err := net.ParseCIDR(stn.ip + "/" + htn.class) + if err != nil { + return nil, fmt.Errorf("Unable to parse ip %s, %s.", stn.ip, err) + } + + // Set interface address in the namespace + if err := veth.SetPeerLinkNetInNs(pid, vethGuestIp, vethGuestIpNet, nil); err != nil { + return nil, fmt.Errorf("Unable to parse ip link in namespace, %s.", err) + } + + return stn, nil + +} + +// Setup the networking inside the child +// Namely setup the loopback interface +// and the veth interface if requested +func NetSetup(stn *SandboxNetwork, htn *HostNetwork) error { + if os.Getpid() != 1 { + panic(errors.New("Cannot use netChildSetup from child.")) + } + + // Bring loopback interface up + lo, err := tenus.NewLinkFrom("lo") + if err != nil { + return fmt.Errorf("Unable to fetch loopback interface, %s.", err) + } + + // Bring the link up + err = lo.SetLinkUp() + if err != nil { + return fmt.Errorf("Unable to bring loopback interface up, %s.", err) + } + + // If required configure veth + if stn.vethGuest != "" { + ifc, err := tenus.NewLinkFrom(stn.vethGuest) + if err == nil { + // Bring the link down to prepare for renaming + if err = ifc.SetLinkDown(); err != nil { + return fmt.Errorf("Unable to bring interface %s down, %s.", stn.vethGuest, err) + } + + // Rename the interface to a standard eth0 (not really a necessity) + if err = tenus.RenameInterface(stn.vethGuest, ozDefaultInterfaceInternal); err != nil { + return fmt.Errorf("Unable to rename interface %s, %s.", stn.vethGuest, err) + } + + // Refetch the interface again as it has changed + ifc, err = tenus.NewLinkFrom(ozDefaultInterfaceInternal) + if err != nil { + return fmt.Errorf("Unable to fetch interface %s, %s.", ozDefaultInterfaceInternal, err) + } + + // Bring the link back up + if err = ifc.SetLinkUp(); err != nil { + return fmt.Errorf("Unable to bring interface %s up, %s.", ozDefaultInterfaceInternal, err) + } + + // Set the link's default gateway + if err = ifc.SetLinkDefaultGw(&htn.gateway); err != nil { + return fmt.Errorf("Unable to set default route %s.", err) + } + + } else { + return fmt.Errorf("Unable to fetch inteface %s, %s.", stn.vethGuest, err) + } + } + + return nil + +} + +// Try to find an unassigned IP address +// Do this by first trying two random IPs or, if that fails, sequentially +func getFreshIP(min, max uint64, log *logging.Logger) string { + var newIP string + + for i := 0; i < ozMaxRandTries; i++ { + newIP = getRandIP(min, max) + if newIP != "" { + break + } + } + + if newIP == "" { + log.Notice("Random IP lookup failed %d times, reverting to sequential select", ozMaxRandTries) + + newIP = getScanIP(min, max) + } + + return newIP + +} + +// Generate a random ip and arping it to see if it is available +// Returns the ip on success or an ip string is the ip is already taken +func getRandIP(min, max uint64) string { + if min > max { + return "" + } + + dstIP := inet_ntoa(uint64(rand.Int63n(int64(max-min))) + min) + + arping.SetTimeout(time.Millisecond * 150) + + _, _, err := arping.PingOverIfaceByName(dstIP, ozDefaultInterfaceBridge) + + if err == arping.ErrTimeout { + return dstIP.String() + } else if err != nil { + return dstIP.String() + } + + return "" + +} + +// Go through all possible ips between min and max +// and arping each one until a free one is found +func getScanIP(min, max uint64) string { + if min > max { + return "" + } + + for i := min; i < max; i++ { + dstIP := inet_ntoa(i) + + arping.SetTimeout(time.Millisecond * 150) + + _, _, err := arping.PingOverIfaceByName(dstIP, ozDefaultInterfaceBridge) + + if err == arping.ErrTimeout { + return dstIP.String() + } else if err != nil { + return dstIP.String() + } + } + + return "" + +} diff --git a/oz-daemon/daemon.go b/oz-daemon/daemon.go index c6d33dd..04a1bb6 100644 --- a/oz-daemon/daemon.go +++ b/oz-daemon/daemon.go @@ -2,13 +2,15 @@ package daemon import ( "fmt" + "os/user" + "syscall" - "github.com/op/go-logging" "github.com/subgraph/oz" "github.com/subgraph/oz/fs" "github.com/subgraph/oz/ipc" - "os/user" - "syscall" + "github.com/subgraph/oz/network" + + "github.com/op/go-logging" ) type daemonState struct { @@ -20,6 +22,7 @@ type daemonState struct { nextDisplay int memBackend *logging.ChannelMemoryBackend backends []logging.Backend + network *network.HostNetwork } func Main() { @@ -30,6 +33,7 @@ func Main() { d.handlePing, d.handleListProfiles, d.handleLaunch, + d.handleInitBridgeNetwork, d.handleListSandboxes, d.handleClean, d.handleLogs, @@ -59,6 +63,15 @@ func initialize() *daemonState { oz.ReapChildProcs(d.log, d.handleChildExit) d.nextSboxId = 1 d.nextDisplay = 100 + + d.log.Info("Initializing bridge networking") + htn, err := network.BridgeInit(d.log) + if err != nil { + d.log.Fatalf("Failed to initialize bridge networking: %+v", err) + } + + d.network = htn + return d } @@ -112,6 +125,12 @@ func (d *daemonState) handleLaunch(msg *LaunchMsg, m *ipc.Message) error { return m.Respond(&OkMsg{}) } +func (d *daemonState) handleInitBridgeNetwork(msg *InitNetworkMsg, m *ipc.Message) error { + d.Debug("Network bridge init message received: %+v", msg) + + return m.Respond(&OkMsg{}) +} + func (d *daemonState) getProfileByIdxOrName(index int, name string) (*oz.Profile, error) { if len(name) == 0 { if index < 1 || index > len(d.profiles) { diff --git a/oz-daemon/launch.go b/oz-daemon/launch.go index e5888d2..ae55d91 100644 --- a/oz-daemon/launch.go +++ b/oz-daemon/launch.go @@ -6,6 +6,7 @@ import ( "github.com/subgraph/oz" "github.com/subgraph/oz/fs" "github.com/subgraph/oz/xpra" + "github.com/subgraph/oz/network" "io" "os" "os/exec" @@ -29,6 +30,7 @@ type Sandbox struct { addr string xpra *xpra.Xpra ready sync.WaitGroup + network *network.SandboxNetwork } /* diff --git a/oz-daemon/protocol.go b/oz-daemon/protocol.go index a5a3500..6e724e0 100644 --- a/oz-daemon/protocol.go +++ b/oz-daemon/protocol.go @@ -35,6 +35,11 @@ type LaunchMsg struct { Name string } +type InitNetworkMsg struct { + Index int "NetworkBridge" + Name string +} + type ListSandboxesMsg struct { _ string "ListSandboxes" }