Skip to content

Commit 6d37a1b

Browse files
committed
feat: host<->container networking
1 parent d4c57ae commit 6d37a1b

File tree

8 files changed

+232
-16
lines changed

8 files changed

+232
-16
lines changed

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Currunt
22

3-
This project is a container runtime, written primarily as a learning exercise, by referencing only the OCI spec, Kernel documentation, blog posts, and educational resources. Source code and documentation of existing container runtimes have not been referenced. External dependencies are minimized to the greatest degree possible.
3+
This project is a container runtime, written primarily as a learning exercise, by referencing only the OCI spec, Kernel documentation, blog posts, and educational resources. No source code/documentation of existing container runtimes or AI has been used. External dependencies are minimized to the greatest degree possible.
44

55
It is being written in two phases:
66

@@ -31,14 +31,17 @@ Goal: "feel like a container runtime"
3131
- [x] support namespaces
3232
- [x] switch from chroot to pivotroot
3333
- [x] track running containers in an index file
34+
- [x] support networking to host
3435

3536
## Phase 1b
3637

3738
Goal: support more nuanced container features that make the magic happen
3839

40+
- more networking
41+
- [ ] bridge with host
42+
- [ ] ingress traffic filters (expose ports)
3943
- [ ] support cgroups
4044
- [ ] support adding/dropping capabilities
41-
- [ ] support networking (and port mapping)
4245
- [ ] use a system location for image storage
4346
- [ ] image caching
4447
- [ ] layer caching
@@ -75,3 +78,4 @@ And other items from the OCI spec
7578
- [overlay fs docs](https://docs.kernel.org/filesystems/overlayfs.html)
7679
- [container in c](https://zserge.com/posts/containers/)
7780
- [namespaces in go](https://medium.com/@teddyking/namespaces-in-go-user-a54ef9476f2a)
81+
- [Scott Lowe's Weblog: Network Namespaces](https://blog.scottlowe.org/2013/09/04/introducing-linux-network-namespaces/)

go.mod

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ module github.com/holysoles/currunt
33
go 1.24.2
44

55
require (
6-
github.com/google/uuid v1.6.0 // indirect
7-
golang.org/x/sys v0.33.0 // indirect
6+
github.com/vishvananda/netlink v1.3.1
7+
golang.org/x/sys v0.33.0
8+
)
9+
10+
require (
11+
github.com/coreos/go-iptables v0.8.0 // indirect
12+
github.com/vishvananda/netns v0.0.5 // indirect
813
)

go.sum

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1+
github.com/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNsPRc=
2+
github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
13
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
24
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
5+
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
6+
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
7+
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
8+
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
9+
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
10+
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
311
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
412
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=

main.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ import (
55
"fmt"
66
"os"
77

8-
"github.com/google/uuid"
9-
108
"github.com/holysoles/currunt/pkg/config"
119
"github.com/holysoles/currunt/pkg/container"
1210
"github.com/holysoles/currunt/pkg/oci"
@@ -25,7 +23,7 @@ func (v *containerVolumes) Set(s string) error {
2523

2624
func main() {
2725
runF := flag.NewFlagSet("run", flag.ExitOnError)
28-
runName := runF.String("name", uuid.New().String(), "name")
26+
runName := runF.String("name", "", "name")
2927
runImg := runF.String("image", "", "image")
3028
runCmd := runF.String("entrypoint", "", "entrypoint")
3129
runArgs := runF.String("cmd", "", "cmd")
@@ -53,6 +51,7 @@ func main() {
5351
spawnGroup := container.UnixId{}
5452
spawnF.Var(&spawnUser, "user", "user")
5553
spawnF.Var(&spawnGroup, "group", "group")
54+
spawnIP := spawnF.String("ip", "", "ip")
5655

5756
if len(os.Args) < 2 {
5857
fmt.Print("invalid usage!")
@@ -96,7 +95,7 @@ func main() {
9695
os.Exit(1)
9796
}
9897
spawnF.Parse(os.Args[2:])
99-
err := execContainer(*spawnCmd, *spawnArgs, spawnEnv, *spawnId, spawnUser, spawnGroup, *spawnWorkDir)
98+
err := execContainer(*spawnCmd, *spawnArgs, spawnEnv, *spawnId, spawnUser, spawnGroup, *spawnWorkDir, *spawnIP)
10099
if err != nil {
101100
fmt.Printf("ERROR: error spawning process for container: %v\n", err)
102101
}

pkg/container/container.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ package container
22

33
import (
44
"fmt"
5+
"math/rand"
56
"os"
67
"path/filepath"
78
"strconv"
89
"strings"
910

10-
"github.com/google/uuid"
1111
"golang.org/x/sys/unix"
1212

1313
"github.com/holysoles/currunt/pkg/oci"
@@ -17,8 +17,17 @@ const (
1717
ARG_DELIMITER = ";"
1818
CONTAINER_LIB = "./containers"
1919
DIR_MASK = 0o700
20+
randChars = "abcdefghijklmnopqrstuvwxyz0123456789"
2021
)
2122

23+
func randString(n int) string {
24+
b := make([]byte, n)
25+
for i := range b {
26+
b[i] = randChars[rand.Int63()%int64(len(randChars))]
27+
}
28+
return string(b)
29+
}
30+
2231
type Env []string
2332

2433
func (e *Env) String() string {
@@ -89,8 +98,11 @@ type Container struct {
8998

9099
func New(name string, config oci.Config) Container {
91100
var c Container
101+
if name == "" {
102+
name = randString(8)
103+
}
92104
c.Name = name
93-
c.Id = uuid.New().String()
105+
c.Id = randString(6)
94106
c.Env = config.Env
95107
c.Entrypoint = cmdLine(config.Entrypoint)
96108
c.Args = cmdLine(config.Cmd)

pkg/container/networking.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package container
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"github.com/coreos/go-iptables/iptables"
8+
"github.com/vishvananda/netlink"
9+
)
10+
11+
func getDefaultLink() (netlink.Link, error) {
12+
var link netlink.Link
13+
routes, err := netlink.RouteList(nil, netlink.FAMILY_ALL)
14+
if err != nil {
15+
return link, err
16+
}
17+
dNet, _ := netlink.ParseAddr("0.0.0.0/0")
18+
for _, r := range routes {
19+
if r.Dst.Network() == dNet.IPNet.Network() {
20+
return netlink.LinkByIndex(r.LinkIndex)
21+
}
22+
}
23+
err = errors.New("did not find a default route")
24+
return link, err
25+
}
26+
27+
func setForwarding(containerLink string, defaultLink string) error {
28+
table, err := iptables.New()
29+
if err != nil {
30+
return err
31+
}
32+
// masq
33+
err = table.Append("nat", "POSTROUTING", "-o", defaultLink, "-j", "MASQUERADE")
34+
if err != nil {
35+
return err
36+
}
37+
38+
// forward
39+
err = table.Append("filter", "FORWARD", "-i", containerLink, "-o", defaultLink, "-j", "ACCEPT")
40+
if err != nil {
41+
return err
42+
}
43+
err = table.Append("filter", "FORWARD", "-i", containerLink, "-o", defaultLink, "-m", "state", "--state", "ESTABLISHED,RELATED", "-j", "ACCEPT")
44+
return err
45+
}
46+
47+
func newBridge(id string, link netlink.Link) error {
48+
bAttrs := netlink.NewLinkAttrs()
49+
bAttrs.Name = "br-" + id
50+
br := &netlink.Bridge{LinkAttrs: bAttrs}
51+
err := netlink.LinkAdd(br)
52+
if err != nil {
53+
return err
54+
}
55+
err = netlink.LinkSetUp(br)
56+
if err != nil {
57+
return err
58+
}
59+
err = netlink.LinkSetMaster(link, br)
60+
return err
61+
}
62+
63+
// func GetIP returns an available IP address for the container
64+
func (c *Container) GetIP() string {
65+
return "10.200.1.2/24"
66+
}
67+
68+
func (c *Container) AttachNet(pid int) error {
69+
pName := "veth-" + c.Id
70+
lName := pName + "2"
71+
attr := netlink.LinkAttrs{Name: lName}
72+
link := netlink.NewVeth(attr)
73+
link.PeerName = pName
74+
err := netlink.LinkAdd(link)
75+
if err != nil {
76+
return err
77+
}
78+
peer, err := netlink.LinkByName(pName)
79+
if err != nil {
80+
return err
81+
}
82+
// TODO IP address lease management
83+
lAddr, err := netlink.ParseAddr("10.200.1.1/24")
84+
if err != nil {
85+
return err
86+
}
87+
err = netlink.AddrAdd(link, lAddr)
88+
if err != nil {
89+
return err
90+
}
91+
err = netlink.LinkSetUp(link)
92+
if err != nil {
93+
return err
94+
}
95+
err = netlink.LinkSetNsPid(peer, pid)
96+
if err != nil {
97+
return err
98+
}
99+
fmt.Println("DEBUG: setup veth and pushed peer into container netns")
100+
// peer gets configured within the namespace (addr, set up)
101+
102+
// bridge for other containers to share the network. unlikely to use for awhile
103+
//newBridge(c.Id, link)
104+
// TODO teardown actions
105+
106+
dLink, err := getDefaultLink()
107+
if err != nil {
108+
err = fmt.Errorf("failed to discover default network interface: %v", err)
109+
return err
110+
}
111+
dLinkName := dLink.Attrs().Name
112+
// TODO we should add routing to common bridge like docker does
113+
// then we can just setup iptables rules once
114+
115+
// setup routing for the veth to the default interface
116+
err = setForwarding(lName, dLinkName)
117+
if err != nil {
118+
err = fmt.Errorf("failed to configure forwarding from host to container network: %v", err)
119+
return err
120+
}
121+
122+
return err
123+
}

run.go

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,23 @@ import (
44
"encoding/json"
55
"fmt"
66
"io"
7+
"net"
78
"os"
89
"os/exec"
910
"path/filepath"
1011
"strings"
12+
"time"
13+
14+
"github.com/vishvananda/netlink"
15+
"golang.org/x/sys/unix"
1116

1217
"github.com/holysoles/currunt/pkg/config"
1318
"github.com/holysoles/currunt/pkg/container"
1419
"github.com/holysoles/currunt/pkg/oci"
15-
"golang.org/x/sys/unix"
20+
)
21+
22+
const (
23+
CREATE_TIMEOUT_SECONDS = 30
1624
)
1725

1826
// func pivotRoot is a wrapper around the syscall for pivot_root. The implementation is based upon usage in the pivot_root manpage: https://man7.org/linux/man-pages/man2/pivot_root.2.html
@@ -154,6 +162,8 @@ func initContainer(cfg config.CurruntConfig, name string, entrypoint string, cmd
154162
c.User.String(),
155163
"--group",
156164
c.Group.String(),
165+
"--ip",
166+
c.GetIP(),
157167
}
158168
spawnArgs = append(spawnArgs, spawnEnv...)
159169

@@ -191,8 +201,20 @@ func initContainer(cfg config.CurruntConfig, name string, entrypoint string, cmd
191201

192202
fmt.Println("DEBUG: starting namespace isolated runtime process")
193203
err = p.Start()
204+
defer p.Process.Kill()
205+
194206
c.Status = "Running"
195-
saveContainer(c)
207+
err = saveContainer(c)
208+
if err != nil {
209+
err = fmt.Errorf("unable to save container state: %v", err)
210+
return c, err
211+
}
212+
213+
err = c.AttachNet(p.Process.Pid)
214+
if err != nil {
215+
err = fmt.Errorf("unable to configure network: %v", err)
216+
return c, err
217+
}
196218

197219
if detach {
198220
p.Process.Release()
@@ -209,7 +231,7 @@ func initContainer(cfg config.CurruntConfig, name string, entrypoint string, cmd
209231
}
210232

211233
// func execContainer finalizes preparing the container in the container ns, and executes the entrypoint. Should be called through initContainer
212-
func execContainer(cmd string, args string, env container.Env, id string, uid container.UnixId, gid container.UnixId, wDir string) error {
234+
func execContainer(cmd string, args string, env container.Env, id string, uid container.UnixId, gid container.UnixId, wDir string, ip string) error {
213235
var cEnv []string
214236
cEnv = append(cEnv, env...)
215237

@@ -218,9 +240,53 @@ func execContainer(cmd string, args string, env container.Env, id string, uid co
218240
if err != nil {
219241
return err
220242
}
221-
pivotRoot(dir)
243+
err = pivotRoot(dir)
244+
if err != nil {
245+
return err
246+
}
247+
248+
err = unix.Sethostname([]byte(id))
249+
if err != nil {
250+
return err
251+
}
252+
253+
// block until we get the network device assigned into the namespace
254+
fmt.Println("DEBUG: waiting for network device to be created..")
255+
var link netlink.Link
256+
257+
var timeoutSec int
258+
for timeoutSec < CREATE_TIMEOUT_SECONDS {
259+
link, err = netlink.LinkByName("veth-" + id)
260+
if err == nil {
261+
break
262+
}
263+
timeoutSec++
264+
time.Sleep(time.Second * 1)
265+
}
266+
fmt.Println("DEBUG: got network device, configuring now..")
267+
// could update link name, but we don't
268+
lAddr, err := netlink.ParseAddr(ip)
269+
if err != nil {
270+
return err
271+
}
272+
err = netlink.AddrAdd(link, lAddr)
273+
if err != nil {
274+
return err
275+
}
276+
err = netlink.LinkSetUp(link)
277+
if err != nil {
278+
return err
279+
}
280+
// default route
281+
//dNet, _ := netlink.ParseIPNet("0.0.0.0/0")
282+
gateway := net.IPv4(10, 200, 1, 1)
283+
dRoute := &netlink.Route{Dst: nil, Gw: gateway}
284+
err = netlink.RouteAdd(dRoute)
285+
if err != nil {
286+
return err
287+
}
222288

223-
unix.Sethostname([]byte(id))
289+
fmt.Println("DEBUG: network device configured")
224290

225291
var pAttr unix.SysProcAttr
226292
// TODO i dont think we need this

state.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ func saveContainer(c container.Container) error {
5858
os.MkdirAll(container.CONTAINER_LIB, 0755)
5959
sFPath := filepath.Join(container.CONTAINER_LIB, "state.json")
6060
var sFile *os.File
61-
fmt.Println("DEBUG: opening state file..")
6261
sFile, err := os.OpenFile(sFPath, os.O_CREATE|os.O_RDWR, 0666)
6362
if err != nil {
6463
return err

0 commit comments

Comments
 (0)