added proposed network plumbing

This commit is contained in:
Eugene Yakubovich
2015-01-12 16:58:03 -08:00
parent 2a5abe00d2
commit 39ac3a2edf
12 changed files with 1018 additions and 0 deletions
+7
View File
@@ -35,3 +35,10 @@ go build -o $GOBIN/rkt ${REPO_PATH}/rkt
echo "Building metadatasvc..."
go build -o $GOBIN/metadatasvc ${REPO_PATH}/metadatasvc
echo "Building network plugins"
for d in network/plugins/*; do
plugin=$(basename $d)
echo " " $plugin
go build -o $GOBIN/$plugin ${REPO_PATH}/$d
done
+134
View File
@@ -0,0 +1,134 @@
package ipam
// ATTN: This is mostly throw away code. It'll be replaced
// by proper ip mgt plugins.
import (
"crypto/rand"
"encoding/binary"
"fmt"
"math/big"
"net"
"strings"
"github.com/coreos/rocket/network/util"
)
type options struct {
ipRange *net.IPNet
ip net.IP
}
func ipAdd(ip net.IP, val uint) net.IP {
n := binary.BigEndian.Uint32(ip.To4())
n += uint32(val)
nip := make([]byte, 4)
binary.BigEndian.PutUint32(nip, n)
return net.IP(nip)
}
func allocIP(ipn *net.IPNet) (*net.IPNet, error) {
ones, bits := ipn.Mask.Size()
zeros := bits - ones
rng := (1 << uint(zeros)) - 2 // (reduce for gw, bcast)
n, err := rand.Int(rand.Reader, big.NewInt(int64(rng)))
if err != nil {
return nil, err
}
offset := uint(n.Uint64() + 1)
return &net.IPNet{
IP: ipAdd(ipn.IP, offset),
Mask: ipn.Mask,
}, nil
}
func deallocIP(ip net.IP) error {
return nil
}
func splitArg(arg string) (k, v string) {
parts := strings.SplitN(arg, "=", 2)
switch len(parts) {
case 1:
k = parts[0]
case 2:
k, v = parts[0], parts[1]
}
return
}
func parseArgs(args string) (*options, error) {
argv := strings.Split(args, ",")
var err error
opts := &options{}
for _, arg := range argv {
k, v := splitArg(arg)
switch k {
case "iprange":
opts.ipRange, err = util.ParseCIDR(v)
if err != nil {
return nil, fmt.Errorf("failed to parse iprange arg (%q): %v", v, err)
}
case "ip":
opts.ip = net.ParseIP(v)
if opts.ip == nil {
return nil, fmt.Errorf("failed to parse ip arg (%q)", v)
}
}
}
return opts, nil
}
func AllocIP(contID, netConf, ifName, args string) (*net.IPNet, net.IP, error) {
opts, err := parseArgs(args)
if err != nil {
return nil, nil, err
}
if opts.ipRange != nil {
ipn, err := allocIP(opts.ipRange)
if err != nil {
return nil, nil, fmt.Errorf("error allocating IP in %v: %v", ipn, err)
}
return ipn, nil, nil
}
n := util.Net{}
if err := util.LoadNet(netConf, &n); err != nil {
return nil, nil, err
}
switch n.IPAlloc.Type {
case "static":
_, ipn, err := net.ParseCIDR(n.IPAlloc.Subnet)
if err != nil {
// TODO: cleanup
return nil, nil, fmt.Errorf("error parsing %q conf: ipAlloc.Subnet: %v")
}
ipn, err = allocIP(ipn)
if err != nil {
// TODO: cleanup
return nil, nil, fmt.Errorf("error allocating IP in %v: %v", ipn, err)
}
return ipn, nil, nil
default:
return nil, nil, fmt.Errorf("unsupported IP allocation type")
}
}
func DeallocIP(contID, netConf, ifName string, ipn *net.IPNet) error {
return nil
}
+137
View File
@@ -0,0 +1,137 @@
package network
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/coreos/rocket/network/util"
)
type NetPlugin struct {
Name string `json:"name,omitempty"`
Endpoint string `json:"endpoint,omitempty"`
Command struct {
Add []string `json:"add,omitempty"`
Del []string `json:"del,omitempty"`
}
}
const RktNetPluginsPath = "/etc/rkt-net-plugins.conf.d"
func LoadNetPlugin(path string) (*NetPlugin, error) {
c, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
np := &NetPlugin{}
if err = json.Unmarshal(c, np); err != nil {
return nil, err
}
return np, nil
}
func LoadNetPlugins() (map[string]*NetPlugin, error) {
plugins := make(map[string]*NetPlugin)
dirents, err := ioutil.ReadDir(RktNetPluginsPath)
switch {
case err == nil:
case os.IsNotExist(err):
return plugins, nil
default:
return nil, err
}
for _, dent := range dirents {
if dent.IsDir() {
continue
}
npPath := filepath.Join(RktNetPluginsPath, dent.Name())
np, err := LoadNetPlugin(npPath)
if err != nil {
log.Printf("Loading %v: %v", npPath, err)
continue
}
plugins[np.Name] = np
}
return plugins, nil
}
func (np *NetPlugin) Add(n *Net, contID, netns, args, ifName string) (*net.IPNet, error) {
switch {
case np.Endpoint != "":
return nil, execHTTP(np.Endpoint, "add", n.Name, contID, netns, n.Filename, args, ifName)
default:
if len(np.Command.Add) == 0 {
return nil, fmt.Errorf("plugin does not define command.add")
}
output, err := execCmd(np.Command.Add, n.Name, contID, netns, n.Filename, args, ifName)
if err != nil {
return nil, err
}
fmt.Printf("plugin's output %q\n", output)
return util.ParseCIDR(output)
}
}
func (np *NetPlugin) Del(n *Net, contID, netns, args, ifName string) error {
switch {
case np.Endpoint != "":
return execHTTP(np.Endpoint, "del", n.Name, contID, netns, n.Filename, args, ifName)
default:
if len(np.Command.Del) == 0 {
return fmt.Errorf("plugin does not define command.del")
}
_, err := execCmd(np.Command.Del, n.Name, contID, netns, n.Filename, args, ifName)
return err
}
}
func execHTTP(ep, cmd, netName, contID, netns, confFile, args, ifName string) error {
return fmt.Errorf("not implemented")
}
func replaceAll(xs []string, what, with string) {
for i, x := range xs {
xs[i] = strings.Replace(x, what, with, -1)
}
}
func execCmd(cmd []string, netName, contID, netns, confFile, args, ifName string) (string, error) {
replaceAll(cmd, "{net-name}", netName)
replaceAll(cmd, "{cont-id}", contID)
replaceAll(cmd, "{netns}", netns)
replaceAll(cmd, "{conf-file}", confFile)
replaceAll(cmd, "{args}", args)
replaceAll(cmd, "{if-name}", ifName)
stdout := &bytes.Buffer{}
c := exec.Command(cmd[0], cmd[1:]...)
c.Stdout = stdout
c.Stderr = os.Stderr
if err := c.Run(); err != nil {
return "", err
}
return stdout.String(), nil
}
+63
View File
@@ -0,0 +1,63 @@
package network
import (
"fmt"
"io/ioutil"
"log"
"os"
"path"
"github.com/coreos/rocket/network/util"
)
const RktNetPath = "/etc/rkt-net.conf.d"
const DefaultIPNet = "172.16.28.0/24"
type Net struct {
util.Net
args string
}
var defaultNet Net
func init() {
defaultNet = Net{
Net: util.Net{
Name: "default",
Type: "veth",
},
args: fmt.Sprintf("default,iprange=%v", DefaultIPNet),
}
}
func LoadNets() ([]Net, error) {
dirents, err := ioutil.ReadDir(RktNetPath)
switch {
case err == nil:
case os.IsNotExist(err):
return nil, nil
default:
return nil, err
}
var nets []Net
for _, dent := range dirents {
if dent.IsDir() {
continue
}
nf := path.Join(RktNetPath, dent.Name())
n := Net{}
if err := util.LoadNet(nf, &n); err != nil {
log.Printf("Error loading %v: %v", nf, err)
continue
}
nets = append(nets, n)
}
nets = append(nets, defaultNet)
return nets, nil
}
+260
View File
@@ -0,0 +1,260 @@
package network
import (
"fmt"
"log"
"net"
"os"
"path/filepath"
"syscall"
"github.com/appc/spec/schema/types"
"github.com/coreos/rocket/Godeps/_workspace/src/github.com/vishvananda/netlink"
"github.com/coreos/rocket/network/util"
)
const (
ifnamePattern = "eth%d"
selfNetNS = "/proc/self/ns/net"
)
type activeNet struct {
Net
ifName string
ipn *net.IPNet
}
type Network struct {
MetadataIP net.IP
contID types.UUID
hostNS *os.File
contNS *os.File
contNSPath string
nets []activeNet
plugins map[string]*NetPlugin
}
func Setup(contID types.UUID) (*Network, error) {
var err error
n := Network{contID: contID}
defer func() {
// cleanup on error
if err != nil {
n.Teardown()
}
}()
if n.hostNS, n.contNS, err = basicNetNS(); err != nil {
return nil, err
}
// we're in contNS!
contNSPath := filepath.Join("/var/lib/rkt/containers", contID.String(), "ns")
if err = bindMountFile(selfNetNS, contNSPath, "net"); err != nil {
return nil, err
}
n.contNSPath = filepath.Join(contNSPath, "net")
n.plugins, err = LoadNetPlugins()
if err != nil {
return nil, fmt.Errorf("error loading plugin definitions: %v", err)
}
nets, err := LoadNets()
if err != nil {
return nil, fmt.Errorf("error loading network definitions: %v", err)
}
err = withNetNS(n.contNS, n.hostNS, func() error {
n.nets, err = setupNets(contID, n.contNSPath, n.plugins, nets)
return err
})
if err != nil {
return nil, err
}
// last net is the default
n.MetadataIP = n.nets[len(n.nets)-1].ipn.IP
return &n, nil
}
func (n *Network) Teardown() {
// teardown everything in reverse order of setup.
// this is called during error case as well so not
// everything maybe setup.
// N.B. better to keep going in case of errors to get as much
// cleaned up as possible
if n.contNS == nil || n.hostNS == nil {
return
}
if err := n.EnterHostNS(); err != nil {
log.Print(err)
return
}
teardownNets(n.contID, n.contNSPath, n.plugins, n.nets)
if n.contNSPath == "" {
return
}
if err := syscall.Unmount(n.contNSPath, 0); err != nil {
log.Print("Error unmounting %q: %v", n.contNSPath, err)
}
}
// sets up new netns with just lo
func basicNetNS() (hostNS, contNS *os.File, err error) {
hostNS, contNS, err = newNetNS()
if err != nil {
err = fmt.Errorf("failed to create new netns: %v", err)
return
}
// we're in contNS!!
if err = loUp(); err != nil {
hostNS.Close()
contNS.Close()
return nil, nil, err
}
return
}
func (n *Network) EnterHostNS() error {
return util.SetNS(n.hostNS, syscall.CLONE_NEWNET)
}
func (n *Network) EnterContNS() error {
return util.SetNS(n.contNS, syscall.CLONE_NEWNET)
}
func setupNets(contID types.UUID, netns string, plugins map[string]*NetPlugin, nets []Net) ([]activeNet, error) {
var err error
active := []activeNet{}
for i, nt := range nets {
plugin, ok := plugins[nt.Type]
if !ok {
err = fmt.Errorf("could not find network plugin %q\n", nt.Type)
break
}
an := activeNet{
Net: nt,
ifName: fmt.Sprintf(ifnamePattern, i),
}
log.Printf("Executing net-plugin %v", nt.Type)
an.ipn, err = plugin.Add(&nt, contID.String(), netns, nt.args, an.ifName)
if err != nil {
err = fmt.Errorf("error adding network %q: %v\n", nt.Name, err)
break
}
active = append(active, an)
}
log.Print("Done executing net plugins")
if err != nil {
teardownNets(contID, netns, plugins, active)
return nil, err
}
return active, nil
}
func teardownNets(contID types.UUID, netns string, plugins map[string]*NetPlugin, nets []activeNet) {
for i := len(nets) - 1; i >= 0; i-- {
nt := nets[i]
plugin := plugins[nt.Type]
err := plugin.Del(&nt.Net, contID.String(), netns, nt.args, nt.ifName)
if err != nil {
log.Printf("Error deleting %q: %v", nt.Name, err)
}
}
}
func newNetNS() (hostNS, childNS *os.File, err error) {
defer func() {
if err != nil {
if hostNS != nil {
hostNS.Close()
}
if childNS != nil {
childNS.Close()
}
}
}()
hostNS, err = os.Open(selfNetNS)
if err != nil {
return
}
if err = syscall.Unshare(syscall.CLONE_NEWNET); err != nil {
return
}
childNS, err = os.Open(selfNetNS)
if err != nil {
util.SetNS(hostNS, syscall.CLONE_NEWNET)
return
}
return
}
// execute f() in tgtNS
func withNetNS(curNS, tgtNS *os.File, f func() error) error {
if err := util.SetNS(tgtNS, syscall.CLONE_NEWNET); err != nil {
return err
}
if err := f(); err != nil {
return err
}
return util.SetNS(curNS, syscall.CLONE_NEWNET)
}
func loUp() error {
lo, err := netlink.LinkByName("lo")
if err != nil {
return fmt.Errorf("failed to lookup lo: %v", err)
}
if err := netlink.LinkSetUp(lo); err != nil {
return fmt.Errorf("failed to set lo up: %v", err)
}
return nil
}
func bindMountFile(src, dstDir, dstFile string) error {
if err := os.MkdirAll(dstDir, 0755); err != nil {
return err
}
dst := filepath.Join(dstDir, dstFile)
// mount point has to be an existing file
f, err := os.Create(dst)
if err != nil {
return err
}
f.Close()
return syscall.Mount(src, dst, "none", syscall.MS_BIND, "")
}
+130
View File
@@ -0,0 +1,130 @@
package main
import (
"fmt"
"log"
"net"
"os"
"runtime"
"strings"
"syscall"
"github.com/coreos/rocket/Godeps/_workspace/src/github.com/vishvananda/netlink"
"github.com/coreos/rocket/network/ipam"
"github.com/coreos/rocket/network/util"
)
func init() {
// this ensures that main runs only on main thread (thread group leader).
// since namespace ops (unshare, setns) are done for a single thread, we
// must ensure that the goroutine does not jump from OS thread to thread
runtime.LockOSThread()
}
func argsHasDefault(args string) bool {
argv := strings.Split(args, ",")
for _, a := range argv {
if a == "default" {
return true
}
}
return false
}
func cmdAdd(contID, netns, netConf, ifName, args string) error {
var hostVethName string
ipn, gw, err := ipam.AllocIP(contID, netConf, ifName, args)
if err != nil {
return err
}
err = util.WithNetNSPath(netns, func(hostNS *os.File) error {
entropy := contID + ifName
hostVeth, contVeth, err := util.SetupVeth(entropy, ifName, ipn, hostNS)
if err != nil {
return err
}
if argsHasDefault(args) {
if err = util.AddDefaultRoute(gw, contVeth); err != nil {
return fmt.Errorf("failed to add default route: %v", err)
}
}
hostVethName = hostVeth.Attrs().Name
return err
})
// hostVeth moved namespaces and will have a new ifindex
hostVeth, err := netlink.LinkByName(hostVethName)
if err != nil {
return fmt.Errorf("failed to lookup %q: %v", hostVeth.Attrs().Name, err)
}
// On the host we route traffic for the allocated IP to the container
ipn.Mask = net.CIDRMask(32, 32)
if err = util.AddRoute(ipn, nil, hostVeth); err != nil {
return fmt.Errorf("failed to add route on host: %v", err)
}
os.Stdout.Write([]byte(ipn.String()))
return nil
}
func cmdDel(contID, netns, netConf, ifName, args string) error {
// switch to the container namespace
contNS, err := os.Open(netns)
if err != nil {
return fmt.Errorf("Failed to open %v: %v", netns, err)
}
if err = util.SetNS(contNS, syscall.CLONE_NEWNET); err != nil {
return fmt.Errorf("Error switching to ns %v: %v", netns, err)
}
iface, err := netlink.LinkByName(ifName)
if err != nil {
return fmt.Errorf("Failed to lookup %q: %v", ifName, err)
}
if err = netlink.LinkDel(iface); err != nil {
return fmt.Errorf("Failed to delete %q: %v", ifName, err)
}
return nil
}
func usage() int {
fmt.Fprintln(os.Stderr, "USAGE: add|del CONTAINER-ID NETNS NET-CONF IF-NAME ARGS")
return 1
}
func main() {
if len(os.Args) != 7 {
os.Exit(usage())
}
var err error
switch os.Args[1] {
case "add":
err = cmdAdd(os.Args[2], os.Args[3], os.Args[4], os.Args[5], os.Args[6])
case "del":
err = cmdDel(os.Args[2], os.Args[3], os.Args[4], os.Args[5], os.Args[6])
default:
os.Exit(usage())
}
if err != nil {
log.Print(err)
os.Exit(1)
}
}
+15
View File
@@ -0,0 +1,15 @@
package util
import (
"net"
)
func ParseCIDR(s string) (*net.IPNet, error) {
ip, ipn, err := net.ParseCIDR(s)
if err != nil {
return nil, err
}
ipn.IP = ip
return ipn, nil
}
+43
View File
@@ -0,0 +1,43 @@
package util
import (
"encoding/json"
"io/ioutil"
"reflect"
)
type Net struct {
Filename string
Name string `json:"name,omitempty"`
Type string `json:"type,omitempty"`
IPAlloc struct {
Type string `json:"type,omitempty"`
Subnet string `json:"subnet,omitempty"`
}
}
func LoadNet(path string, n interface{}) error {
c, err := ioutil.ReadFile(path)
if err != nil {
return err
}
if err = json.Unmarshal(c, n); err != nil {
return err
}
// populate n.Filename if exists
v := reflect.ValueOf(n)
if v.Kind() == reflect.Ptr {
v = v.Elem()
if v.Kind() == reflect.Struct {
if fn := v.FieldByName("Filename"); fn.IsValid() {
if fn.Type().Kind() == reflect.String && fn.CanSet() {
fn.SetString(path)
}
}
}
}
return nil
}
+80
View File
@@ -0,0 +1,80 @@
package util
import (
"encoding/json"
"io/ioutil"
"reflect"
"testing"
)
func saveToTemp(v interface{}) (string, error) {
f, err := ioutil.TempFile("", "net")
if err != nil {
return "", err
}
defer f.Close()
return f.Name(), json.NewEncoder(f).Encode(v)
}
func TestNet(t *testing.T) {
expected := Net{
Name: "mynet",
Type: "veth",
}
expected.IPAlloc.Type = "static"
expected.IPAlloc.Subnet = "10.1.2.0/24"
fn, err := saveToTemp(expected)
if err != nil {
t.Fatal(err)
}
expected.Filename = fn
actual := Net{}
if err = LoadNet(fn, &actual); err != nil {
t.Fatal(err)
}
if expected.Filename != actual.Filename {
t.Errorf("Filename mismatch: expected=%q; actual=%q", expected.Filename, actual.Filename)
}
if !reflect.DeepEqual(expected, actual) {
t.Errorf("Mismatch: expected=%#v; actual=%#v", expected, actual)
}
}
type MyNet struct {
Net
}
func TestNetEmbedded(t *testing.T) {
expected := MyNet{
Net: Net{
Name: "mynet",
Type: "veth",
},
}
expected.IPAlloc.Type = "static"
expected.IPAlloc.Subnet = "10.1.2.0/24"
fn, err := saveToTemp(expected)
if err != nil {
t.Fatal(err)
}
expected.Filename = fn
actual := MyNet{}
if err = LoadNet(fn, &actual); err != nil {
t.Fatal(err)
}
if expected.Filename != actual.Filename {
t.Errorf("Filename mismatch: expected=%q; actual=%q", expected.Filename, actual.Filename)
}
if !reflect.DeepEqual(expected, actual) {
t.Errorf("Mismatch: expected=%#v; actual=%#v", expected, actual)
}
}
+22
View File
@@ -0,0 +1,22 @@
package util
import (
"net"
"github.com/coreos/rocket/Godeps/_workspace/src/github.com/vishvananda/netlink"
)
func AddDefaultRoute(gw net.IP, dev netlink.Link) error {
_, defNet, _ := net.ParseCIDR("0.0.0.0/0")
return AddRoute(defNet, gw, dev)
}
func AddRoute(ipn *net.IPNet, gw net.IP, dev netlink.Link) error {
return netlink.RouteAdd(&netlink.Route{
LinkIndex: dev.Attrs().Index,
Scope: netlink.SCOPE_UNIVERSE,
Dst: ipn,
Gw: gw,
})
}
+60
View File
@@ -0,0 +1,60 @@
package util
import (
"fmt"
"os"
"runtime"
"syscall"
)
var setNsMap = map[string]uintptr{
"386": 346,
"amd64": 308,
"arm": 374,
}
func SetNS(f *os.File, flags uintptr) error {
if runtime.GOOS != "linux" {
return fmt.Errorf("unsupported OS: %s", runtime.GOOS)
}
trap, ok := setNsMap[runtime.GOARCH]
if !ok {
return fmt.Errorf("unsupported arch: %s", runtime.GOARCH)
}
_, _, err := syscall.RawSyscall(trap, f.Fd(), flags, 0)
if err != 0 {
return err
}
return nil
}
func WithNetNSPath(nspath string, f func(*os.File) error) error {
// save a handle to current (host) network namespace
thisNS, err := os.Open("/proc/self/ns/net")
if err != nil {
return fmt.Errorf("Failed to open /proc/self/ns/net: %v", err)
}
// switch to the container namespace
ns, err := os.Open(nspath)
if err != nil {
return fmt.Errorf("Failed to open %v: %v", nspath, err)
}
if err = SetNS(ns, syscall.CLONE_NEWNET); err != nil {
return fmt.Errorf("Error switching to ns %v: %v", nspath, err)
}
if err = f(thisNS); err != nil {
return err
}
// switch back
if err = SetNS(thisNS, syscall.CLONE_NEWNET); err != nil {
return err
}
return nil
}
+67
View File
@@ -0,0 +1,67 @@
package util
import (
"crypto/sha512"
"fmt"
"net"
"os"
"github.com/coreos/rocket/Godeps/_workspace/src/github.com/vishvananda/netlink"
)
func makeVeth(name, peer string) (netlink.Link, error) {
veth := &netlink.Veth{
LinkAttrs: netlink.LinkAttrs{
Name: name,
Flags: net.FlagUp,
},
PeerName: peer,
}
if err := netlink.LinkAdd(veth); err != nil {
return nil, err
}
return veth, nil
}
func hash(s string) string {
h := sha512.New()
h.Write([]byte(s))
return fmt.Sprintf("%x", h.Sum(nil))
}
// Should be in container netns
func SetupVeth(entropy, contVethName string, ipn *net.IPNet, hostNS *os.File) (hostVeth, contVeth netlink.Link, err error) {
hostVethName := "rk" + hash(entropy)[:6]
hostVeth, err = makeVeth(hostVethName, contVethName)
if err != nil {
err = fmt.Errorf("failed to make veth pair: %v", err)
return
}
if err = netlink.LinkSetNsFd(hostVeth, int(hostNS.Fd())); err != nil {
err = fmt.Errorf("failed to move veth to root netns: %v", err)
return
}
contVeth, err = netlink.LinkByName(contVethName)
if err != nil {
err = fmt.Errorf("failed to lookup %q: %v", contVethName, err)
return
}
if err = netlink.LinkSetUp(contVeth); err != nil {
err = fmt.Errorf("failed to set eth0 up: %v", err)
return
}
if ipn != nil {
addr := &netlink.Addr{ipn, ""}
if err = netlink.AddrAdd(contVeth, addr); err != nil {
err = fmt.Errorf("failed to add IP addr to veth: %v", err)
return
}
}
return
}