Support plugin network stack

This commit supports a third-party network stack as a plugin stack for
gVisor.

The overall plugin package structure is the following:

- pkg/sentry/socket/plugin:
  Interfaces for initializing plugin network stack. It will be used
  in network setting up during sandbox creating.

- pkg/sentry/socket/plugin/stack:
  Glue layer for plugin stack's socket and stack ops with sentry. It
  will also register plugin stack operations if imported.

- pkg/sentry/socket/plugin/cgo:
  Interfaces defined in C for plugin network stack to support.

To build target runsc-plugin-stack, which imports
pkg/sentry/socket/plugin/stack package and enables CGO:

bazel build --config=plugin-tldk runsc:runsc-plugin-stack

(i.e. --config=plugin-tldk indicates that using TLDK as plugin stack)

By using runsc-plugin-stack binary and setting "--network=plugin" in
runtimeArgs, user can use third-party network stack instead of
netstack embedded in gVisor to get better network performance.

Redis benchmark with following setups:
1. KVM platform
2. 4 physical cores for target pod
3. target pod as redis server

Runc:
$redis-benchmark -h [target ip] -n 100000 -t get,set -q
SET: 115207.38 requests per second, p50=0.215 msec
GET: 92336.11 requests per second, p50=0.279 msec

$redis-benchmark -h [target ip] -n 100000 -t get,set -q
SET: 113895.21 requests per second, p50=0.247 msec
GET: 96899.23 requests per second, p50=0.271 msec

$redis-benchmark -h [target ip] -n 100000 -t get,set -q
SET: 126582.27 requests per second, p50=0.199 msec
GET: 95969.28 requests per second, p50=0.271 msec

Runsc with plugin stack:
$redis-benchmark -h [target ip] -n 100000 -t get,set -q
SET: 123915.74 requests per second, p50=0.343 msec
GET: 115473.45 requests per second, p50=0.335 msec

$redis-benchmark -h [target ip] -n 100000 -t get,set -q
SET: 120918.98 requests per second, p50=0.351 msec
GET: 117647.05 requests per second, p50=0.351 msec

$redis-benchmark -h [target ip] -n 100000 -t get,set -q
SET: 119904.08 requests per second, p50=0.367 msec
GET: 112739.57 requests per second, p50=0.375 msec

Runsc with netstack:
$redis-benchmark -h [target ip] -n 100000 -t get,set -q
SET: 59952.04 requests per second, p50=0.759 msec
GET: 61162.08 requests per second, p50=0.631 msec

$redis-benchmark -h [target ip] -n 100000 -t get,set -q
SET: 52219.32 requests per second, p50=0.719 msec
GET: 58719.91 requests per second, p50=0.663 msec

$redis-benchmark -h [target ip] -n 100000 -t get,set -q
SET: 59952.04 requests per second, p50=0.751 msec
GET: 60827.25 requests per second, p50=0.751 msec

Updates https://github.com/google/gvisor/issues/9266

Co-developed-by: Tianyu Zhou <wentong.zty@antgroup.com>
Signed-off-by: Anqi Shen <amy.saq@antgroup.com>
This commit is contained in:
Anqi Shen
2024-07-12 09:10:13 +00:00
parent 81f564835e
commit 56f2530dad
42 changed files with 2597 additions and 0 deletions
+45
View File
@@ -0,0 +1,45 @@
package(licenses = ["notice"])
load("//tools:defs.bzl", "go_library")
go_library(
name = "stack",
srcs = [
"notifier.go",
"provider.go",
"readwriter.go",
"socket.go",
"stack.go",
"util.go",
],
visibility = ["//visibility:public"],
deps = [
"//pkg/abi/linux",
"//pkg/abi/linux/errno",
"//pkg/binary",
"//pkg/context",
"//pkg/errors/linuxerr",
"//pkg/hostarch",
"//pkg/marshal",
"//pkg/marshal/primitive",
"//pkg/safemem",
"//pkg/sentry/arch",
"//pkg/sentry/fsimpl/sockfs",
"//pkg/sentry/inet",
"//pkg/sentry/kernel",
"//pkg/sentry/kernel/auth",
"//pkg/sentry/kernel/time",
"//pkg/sentry/socket",
"//pkg/sentry/socket/plugin",
"//pkg/sentry/socket/plugin/cgo",
"//pkg/sentry/unimpl",
"//pkg/sentry/vfs",
"//pkg/syserr",
"//pkg/tcpip",
"//pkg/tcpip/network/ipv4",
"//pkg/tcpip/network/ipv6",
"//pkg/usermem",
"//pkg/waiter",
"@org_golang_x_sys//unix:go_default_library",
],
)
+165
View File
@@ -0,0 +1,165 @@
// Copyright 2023 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package stack
import (
"fmt"
"runtime"
"sync"
"syscall"
"golang.org/x/sys/unix"
"gvisor.dev/gvisor/pkg/sentry/socket/plugin"
"gvisor.dev/gvisor/pkg/sentry/socket/plugin/cgo"
"gvisor.dev/gvisor/pkg/waiter"
)
// Notifier holds all the state necessary to issue notifications when
// IO events occur on the observed FDs in plugin stack.
type Notifier struct {
// the epoll FD used to register for io notifications.
epFD int32
// mu protects eventMap.
mu sync.Mutex
// eventMap maps file descriptors to their notification queues
// and waiting status.
eventMap map[uint32]*plugin.EventInfo
}
const (
MaxEpollEvents = 128
SleepInMsecond = 100
)
// NewNotifier initialize the event notifier for plugin stack.
// It will allocate a eventMap with fd as key and corresponding eventInfo
// as value and start a goroutine waiting the arrival of events.
func NewNotifier() *Notifier {
ioInit := make(chan int32)
n := &Notifier{
eventMap: make(map[uint32]*plugin.EventInfo),
}
go n.waitAndNotify(ioInit)
epFD := <-ioInit
if epFD < 0 {
return nil
}
return n
}
// AddFD implements plugin.PluginNotifier.AddFD.
func (n *Notifier) AddFD(fd uint32, eventInfo *plugin.EventInfo) {
n.mu.Lock()
defer n.mu.Unlock()
// Panic if we're already notifying on this FD.
if _, ok := n.eventMap[fd]; ok {
panic(fmt.Sprintf("File descriptor %d added twice", fd))
}
// We have nothing to wait for at the moment. Just add it to the map.
n.eventMap[fd] = eventInfo
}
// RemoveFD implements plugin.PluginNotifier.RemoveFD.
func (n *Notifier) RemoveFD(fd uint32) {
n.mu.Lock()
defer n.mu.Unlock()
delete(n.eventMap, fd)
}
// UpdateFD implements plugin.PluginNotifier.UpdateFD.
func (n *Notifier) UpdateFD(fd uint32) {
n.mu.Lock()
defer n.mu.Unlock()
if eventInfo, ok := n.eventMap[fd]; ok {
n.waitFD(fd, eventInfo)
}
}
// waitAndNotify loops waiting for io event notifications from the epoll
// object. Once notifications arrive, they are dispatched to the
// registered queue.
func (n *Notifier) waitAndNotify(ioInit chan int32) error {
// plugin stack leverages TLS varaibles, so bind this goroutine with
// one specific OS thread
runtime.LockOSThread()
// If current thread is not the main thread, change the thread name.
if syscall.Getpid() != syscall.Gettid() {
threadName := []byte("io-thread\x00")
if err := unix.Prctl(unix.PR_SET_NAME, uintptr(cgo.GetPtr(threadName)), 0, 0, 0); err != nil {
return err
}
}
n.epFD = int32(cgo.EpollCreate())
ioInit <- n.epFD
var events [MaxEpollEvents]syscall.EpollEvent
for {
num := cgo.EpollWait(n.epFD, events[:], MaxEpollEvents, SleepInMsecond)
if num <= 0 {
continue
}
n.mu.Lock()
for i := 0; i < num; i++ {
h := uint32(events[i].Fd)
eventInfo, ok := n.eventMap[h]
if !ok {
continue
}
ev := waiter.EventMask(events[i].Events)
eventInfo.Ready |= ev & (eventInfo.Mask | waiter.EventErr | waiter.EventHUp)
// When an error occurred, invoke all events
if ev&(waiter.EventErr|waiter.EventHUp) != 0 {
ev |= waiter.EventIn | waiter.EventOut
}
eventInfo.Wq.Notify(ev)
}
n.mu.Unlock()
}
}
func (n *Notifier) waitFD(fd uint32, eventInfo *plugin.EventInfo) {
mask := eventInfo.Wq.Events()
eventInfo.Mask = mask
if !eventInfo.Waiting && mask == 0 {
return
}
switch {
case !eventInfo.Waiting && mask != 0:
cgo.EpollCtl(n.epFD, syscall.EPOLL_CTL_ADD, fd, uint32(mask))
eventInfo.Waiting = true
case eventInfo.Waiting && mask == 0:
cgo.EpollCtl(n.epFD, syscall.EPOLL_CTL_DEL, fd, uint32(mask))
eventInfo.Ready = 0
eventInfo.Waiting = false
case eventInfo.Waiting && mask != 0:
cgo.EpollCtl(n.epFD, syscall.EPOLL_CTL_MOD, fd, uint32(mask))
eventInfo.Ready &= mask
}
}
+105
View File
@@ -0,0 +1,105 @@
// Copyright 2023 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package stack
import (
"syscall"
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/sentry/kernel"
"gvisor.dev/gvisor/pkg/sentry/kernel/auth"
"gvisor.dev/gvisor/pkg/sentry/socket"
"gvisor.dev/gvisor/pkg/sentry/socket/plugin/cgo"
"gvisor.dev/gvisor/pkg/sentry/vfs"
"gvisor.dev/gvisor/pkg/syserr"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
"gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
)
type provider struct {
family int
netProto tcpip.NetworkProtocolNumber
}
// Socket creates a new socket object for the AF_INET or AF_INET6 family.
func (p *provider) Socket(t *kernel.Task, skType linux.SockType, protocol int) (*vfs.FileDescription, *syserr.Error) {
// Fail right away if there is no plugin stack registered.
ctx := t.NetworkContext()
if ctx == nil {
return nil, nil
}
_, ok := ctx.(*Stack)
if !ok {
return nil, nil
}
// Only accept TCP and UDP.
stype := skType & linux.SOCK_TYPE_MASK
switch stype {
case syscall.SOCK_STREAM:
switch protocol {
case 0, syscall.IPPROTO_TCP:
default:
return nil, syserr.ErrProtocolNotSupported
}
case syscall.SOCK_DGRAM:
switch protocol {
case 0, syscall.IPPROTO_UDP:
default:
return nil, syserr.ErrProtocolNotSupported
}
case syscall.SOCK_RAW:
// Raw sockets require CAP_NET_RAW.
creds := auth.CredentialsFromContext(t)
if !creds.HasCapability(linux.CAP_NET_RAW) {
return nil, syserr.ErrPermissionDenied
}
default:
return nil, syserr.ErrSocketNotSupported
}
handle := cgo.Socket(p.family, int(skType), protocol)
if handle < 0 {
return nil, int2err(handle)
}
fd, err := newSocket(t, p.family, skType, protocol, stack.notifier, int(handle), uint32(skType&syscall.SOCK_NONBLOCK))
return fd, err
}
// Pair just returns nil sockets (not supported).
func (*provider) Pair(*kernel.Task, linux.SockType, int) (*vfs.FileDescription, *vfs.FileDescription, *syserr.Error) {
return nil, nil, nil
}
func init() {
// Providers backed by plugin stack.
p := []provider{
{
family: linux.AF_INET,
netProto: ipv4.ProtocolNumber,
},
{
family: linux.AF_INET6,
netProto: ipv6.ProtocolNumber,
},
}
for i := range p {
socket.RegisterProvider(p[i].family, &p[i])
}
}
@@ -0,0 +1,110 @@
// Copyright 2023 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package stack
import (
"sync"
"syscall"
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/safemem"
"gvisor.dev/gvisor/pkg/sentry/socket/plugin/cgo"
)
type pluginStackRW struct {
handle uint32
// Represent both input and output flags.
flags uint32
// Reused as msg_control for read.
to []byte
iovs [3]syscall.Iovec
}
var pluginStackRWPool = sync.Pool{
New: func() interface{} {
return &pluginStackRW{}
},
}
func getReadWriter(handle uint32) *pluginStackRW {
rw := pluginStackRWPool.Get().(*pluginStackRW)
rw.handle = handle
return rw
}
func putReadWriter(rw *pluginStackRW) {
*rw = pluginStackRW{}
pluginStackRWPool.Put(rw)
}
// ReadToBlocks implements safemem.Reader.ReadToBlocks.
func (rw *pluginStackRW) ReadToBlocks(dsts safemem.BlockSeq) (uint64, error) {
// Set MSG_DONTWAIT flag to avoid blocking in plugin stack.
flags := int(rw.flags) & ^linux.MSG_DONTWAIT
if len(rw.to) != 0 || flags != 0 {
iovs := iovecsFromBlockSeq(dsts, rw)
rc, _, lc, mflags := cgo.Recvmsg(rw.handle, iovs, nil, rw.to, int(rw.flags))
if rc >= 0 {
rw.to = rw.to[:lc]
rw.flags = uint32(mflags)
}
return translateReturn(rc)
}
var rc int64
if dsts.IsEmpty() {
rc = 0
} else if dsts.NumBlocks() == 1 {
rc = cgo.Read(rw.handle, dsts.Head().Addr(), dsts.Head().Len())
} else {
rc = cgo.Readv(rw.handle, iovecsFromBlockSeq(dsts, rw))
}
return translateReturn(rc)
}
// WriteFromBlocks implements safemem.Writer.WriteFromBlocks.
//
// Preconditions: rw.d.metadataMu must be locked.
func (rw *pluginStackRW) WriteFromBlocks(srcs safemem.BlockSeq) (uint64, error) {
var rc int64
if rw.to != nil {
if srcs.IsEmpty() {
// Invoke plugin stack checking whether there is any error to report
// on target socket which sends 0-length data.
rc = cgo.Sendto(rw.handle, 0, 0, 0, rw.to)
} else if srcs.NumBlocks() == 1 {
rc = cgo.Sendto(rw.handle, srcs.Head().Addr(), srcs.Head().Len(), 0, rw.to)
} else {
iovs := iovecsFromBlockSeq(srcs, rw)
rc = cgo.Sendmsg(rw.handle, iovs, rw.to, 0)
}
} else {
if srcs.IsEmpty() {
// Invoke plugin stack checking whether there is any error to report
// on target socket which sends 0-length data.
rc = cgo.Write(rw.handle, 0, 0)
} else if srcs.NumBlocks() == 1 {
rc = cgo.Write(rw.handle, srcs.Head().Addr(), srcs.Head().Len())
} else {
rc = cgo.Writev(rw.handle, iovecsFromBlockSeq(srcs, rw))
}
}
return translateReturn(rc)
}
File diff suppressed because it is too large Load Diff
+86
View File
@@ -0,0 +1,86 @@
// Copyright 2023 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package stack provides an implementation of plugin.PluginStack
// interface and an implementation of socket.Socket interface.
//
// It glues sentry interfaces with plugin netstack interfaces defined in cgo.
package stack
import (
"gvisor.dev/gvisor/pkg/errors/linuxerr"
"gvisor.dev/gvisor/pkg/sentry/inet"
"gvisor.dev/gvisor/pkg/sentry/socket/plugin"
"gvisor.dev/gvisor/pkg/sentry/socket/plugin/cgo"
)
// Stack is a struct that interacts with third-party network stack.
// It implements inet.Stack and plugin.PluginStack.
type Stack struct {
inet.Stack
notifier *Notifier
}
var stack *Stack
// Init implements plugin.PluginStack.Init.
func (s *Stack) Init(args *plugin.InitStackArgs) error {
if err := cgo.InitStack(args.InitStr, args.FDs); err != nil {
return err
}
s.notifier = NewNotifier()
stack = s
return nil
}
// PreInit implements plugin.PluginStack.PreInit.
func (s *Stack) PreInit(args *plugin.PreInitStackArgs) (string, []int, error) {
return cgo.PreInitStack(args.Pid)
}
// Interfaces implements inet.Stack.Interfaces.
func (s *Stack) Interfaces() map[int32]inet.Interface {
// TODO: support Interfaces
return make(map[int32]inet.Interface)
}
// InterfaceAddrs implements inet.Stack.InterfaceAddrs.
func (s *Stack) InterfaceAddrs() map[int32][]inet.InterfaceAddr {
// TODO: support InterfaceAddrs
return make(map[int32][]inet.InterfaceAddr)
}
// AddInterfaceAddr implements inet.Stack.AddInterfaceAddr.
func (s *Stack) AddInterfaceAddr(idx int32, addr inet.InterfaceAddr) error {
return linuxerr.EACCES
}
// RemoveInterfaceAddr implements inet.Stack.RemoveInterfaceAddr.
func (s *Stack) RemoveInterfaceAddr(int32, inet.InterfaceAddr) error {
return linuxerr.EACCES
}
// SupportsIPv6 implements Stack.SupportsIPv6.
func (s *Stack) SupportsIPv6() bool {
return true
}
// Destroy implements inet.Stack.Destroy.
func (*Stack) Destroy() {
}
func init() {
plugin.RegisterPluginStack(&Stack{})
}
+91
View File
@@ -0,0 +1,91 @@
// Copyright 2023 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package stack
import (
"net"
"syscall"
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/abi/linux/errno"
"gvisor.dev/gvisor/pkg/hostarch"
"gvisor.dev/gvisor/pkg/safemem"
"gvisor.dev/gvisor/pkg/sentry/inet"
"gvisor.dev/gvisor/pkg/sentry/socket"
"gvisor.dev/gvisor/pkg/sentry/socket/plugin/cgo"
"gvisor.dev/gvisor/pkg/syserr"
)
func int2err(from int64) *syserr.Error {
if from >= 0 {
return nil
}
if (-from) == errno.EAGAIN {
return syserr.ErrWouldBlock
}
return syserr.FromHost(syscall.Errno(-from))
}
func translateReturn(ret int64) (uint64, error) {
if ret < 0 {
return 0, int2err(ret).ToError()
} else if ret == 0 {
return 0, nil
} else {
return uint64(ret), nil
}
}
func copyAddrOut(ifr *linux.IFReq, ifaceAddr *inet.InterfaceAddr) {
hostarch.ByteOrder.PutUint16(ifr.Data[0:2], uint16(ifaceAddr.Family))
hostarch.ByteOrder.PutUint16(ifr.Data[2:4], 0) // port
if ifaceAddr.Family == linux.AF_INET {
copy(ifr.Data[4:8], net.IP(ifaceAddr.Addr).To4()[:4])
} else {
copy(ifr.Data[8:24], ifaceAddr.Addr[:16])
}
}
func iovecsFromBlockSeq(bs safemem.BlockSeq, rw *pluginStackRW) []syscall.Iovec {
var iovs []syscall.Iovec
if rw != nil {
// Reuse the old buffer and set length to zero.
iovs = rw.iovs[:0]
}
for ; !bs.IsEmpty(); bs = bs.Tail() {
b := bs.Head()
iovs = append(iovs, syscall.Iovec{
Base: &b.ToSlice()[0],
Len: uint64(b.Len()),
})
}
return iovs
}
func buildControlMessage(controlData []byte) *socket.ControlMessages {
controlMessages := socket.ControlMessages{}
if len(controlData) >= 28 {
timebytes := controlData[12:]
timeval := (*linux.Timeval)(cgo.GetPtr(timebytes))
m := socket.IPControlMessages{
HasTimestamp: true,
Timestamp: timeval.ToTime(),
}
controlMessages.IP = m
}
return &controlMessages
}