Repository: threefoldtech/zos
Pull request #1879 (2023-01-03 14:24:51)

Adds cloud-console integration
Signed-off-by: Ashraf Fouda <ashraf.m.fouda@gmail.com>

xmonader: I added some minor comments and questions, also please make sure to add docs file that explains the cloud console flow 
muhamadazmy: BLOCKED ON: https://github.com/LeeSmet/cloud-console/issues/3

Patch:
diff --git a/.github/workflows/bins.yaml b/.github/workflows/bins.yaml
index d0d5edf6..55cc4468 100644
--- a/.github/workflows/bins.yaml
+++ b/.github/workflows/bins.yaml
@@ -90,6 +90,12 @@ jobs:
       package: vector
     secrets:
       token: ${{ secrets.HUB_JWT }}
+  nnc:
+    uses: ./.github/workflows/bin-package.yaml
+    with:
+      package: nnc
+    secrets:
+      token: ${{ secrets.HUB_JWT }}
   tpm:
     uses: ./.github/workflows/bin-package-18.04.yaml
     with:
@@ -102,3 +108,9 @@ jobs:
       package: qsfs
     secrets:
       token: ${{ secrets.HUB_JWT }}
+  cloudconsole:
+    uses: ./.github/workflows/bin-package.yaml
+    with:
+      package: cloudconsole
+    secrets:
+      token: ${{ secrets.HUB_JWT }}
diff --git a/bins/packages/cloudconsole/cloudconsole.sh b/bins/packages/cloudconsole/cloudconsole.sh
new file mode 100644
index 00000000..072df7e5
--- /dev/null
+++ b/bins/packages/cloudconsole/cloudconsole.sh
@@ -0,0 +1,34 @@
+CLOUDCONSOLE_VERSION="v0.1.1"
+CLOUDCONSOLE_CHECKSUM="c018c335ea67424ebd00165b69c42fe2"
+CLOUDCONSOLE_LINK="https://github.com/threefoldtech/cloud-console/releases/download/${CLOUDCONSOLE_VERSION}/cloud-console"
+
+
+download_cloudconsole() {
+    echo "downloading cloud-console"
+    download_file ${CLOUDCONSOLE_LINK} ${CLOUDCONSOLE_CHECKSUM} cloud-console-${CLOUDCONSOLE_VERSION}
+}
+
+
+prepare_cloudconsole() {
+    echo "[+] prepare cloud-console"
+    github_name "cloud-console-${CLOUDCONSOLE_VERSION}"
+}
+
+install_cloudconsole() {
+    echo "[+] install cloud-console"
+
+    mkdir -p "${ROOTDIR}/usr/bin"
+
+    cp ${DISTDIR}/cloud-console-${CLOUDCONSOLE_VERSION} ${ROOTDIR}/usr/bin/cloud-console
+    chmod +x ${ROOTDIR}/usr/bin/*
+}
+
+build_cloudconsole() {
+    pushd "${DISTDIR}"
+
+    download_cloudconsole
+    prepare_cloudconsole
+    install_cloudconsole
+
+    popd
+}
diff --git a/bins/packages/nnc/nnc.sh b/bins/packages/nnc/nnc.sh
new file mode 100644
index 00000000..c7ac3cde
--- /dev/null
+++ b/bins/packages/nnc/nnc.sh
@@ -0,0 +1,33 @@
+NNC_VERSION="1.0.0-rc2"
+NNC_CHECKSUM="7fbadaae30c19b79e6348bf1bd1d29fc"
+NNC_LINK="https://github.com/threefoldtech/nnc/releases/download/v${NNC_VERSION}/nnc"
+
+download_nnc() {
+    echo "download nnc"
+    download_file ${NNC_LINK} ${NNC_CHECKSUM} nnc-peer
+}
+
+prepare_nnc() {
+    echo "[+] prepare nnc"
+    github_name "nnc-${NNC_VERSION}"
+}
+
+install_nnc() {
+    echo "[+] install nnc"
+
+    mkdir -p "${ROOTDIR}/bin"
+
+    cp ${DISTDIR}/nnc-peer ${ROOTDIR}/bin/nnc
+    chmod +x ${ROOTDIR}/bin/*
+    strip ${ROOTDIR}/bin/*
+}
+
+build_nnc() {
+    pushd "${DISTDIR}"
+
+    download_nnc
+    popd
+
+    prepare_nnc
+    install_nnc
+}
diff --git a/cmds/modules/provisiond/main.go b/cmds/modules/provisiond/main.go
index b6f93e99..fc1c548f 100644
--- a/cmds/modules/provisiond/main.go
+++ b/cmds/modules/provisiond/main.go
@@ -1,9 +1,12 @@
 package provisiond
 
 import (
+	"bytes"
 	"context"
 	"crypto/ed25519"
+	"fmt"
 	"os"
+	"os/exec"
 	"path/filepath"
 	"time"
 
@@ -66,27 +69,78 @@ var Module cli.Command = cli.Command{
 			Usage: "connection string to the message `BROKER`",
 			Value: "unix:///var/run/redis.sock",
 		},
-		&cli.StringFlag{
-			Name:  "http",
-			Usage: "http listen address",
-			Value: ":2021",
+		&cli.BoolFlag{
+			Name:  "integrity",
+			Usage: "run some integrity checks on some files",
 		},
 	},
 	Action: action,
 }
 
+// integrityChecks are started in a separate process because
+// we found out that some weird db corruption causing the process
+// to receive a SIGBUS error
+// while we can catch the sigbus and handle it ourselves i thought
+// it's better to do it in a separate process to always have a clean
+// state
+func integrityChecks(ctx context.Context, rootDir string) error {
+	err := ReportChecks(filepath.Join(rootDir, metricsStorageDB))
+
+	if err != nil {
+		fmt.Fprintln(os.Stderr, err)
+	}
+
+	return err
+}
+
+// runChecks starts provisiond with the special flag `--integrity` which runs some
+// checks and return an error if checks did not pass.
+// if an error is received the db files are cleaned
+func runChecks(ctx context.Context, rootDir string) error {
+	log.Info().Msg("run integrity checks")
+	ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
+	defer cancel()
+
+	cmd := exec.CommandContext(ctx, os.Args[0], "--root", rootDir, "--integrity")
+	var buf bytes.Buffer
+	cmd.Stderr = &buf
+
+	cmd.CombinedOutput()
+	err := cmd.Run()
+	if err == context.Canceled {
+		return err
+	} else if err == nil {
+		return nil
+	}
+
+	log.Error().Str("stderr", buf.String()).Err(err).Msg("integrity check failed, resetting rrd db")
+
+	// other error, we can try to clean up and continue
+	return os.RemoveAll(filepath.Join(rootDir, metricsStorageDB))
+}
+
 func action(cli *cli.Context) error {
 	var (
 		msgBrokerCon string = cli.String("broker")
 		rootDir      string = cli.String("root")
+		integrity    bool   = cli.Bool("integrity")
 	)
 
 	ctx, _ := utils.WithSignal(context.Background())
 
+	if integrity {
+		return integrityChecks(ctx, rootDir)
+	}
+
 	utils.OnDone(ctx, func(_ error) {
 		log.Info().Msg("shutting down")
 	})
 
+	// run integrityChecks
+	if err := runChecks(ctx, rootDir); err != nil {
+		return errors.Wrap(err, "error running integrity checks")
+	}
+
 	// keep checking if limited-cache flag is set
 	if app.CheckFlag(app.LimitedCache) {
 		log.Error().Msg("failed cache reservation! Retrying every 30 seconds...")
@@ -365,6 +419,8 @@ func action(cli *cli.Context) error {
 
 	// also spawn the capacity reporter
 	go func() {
+		defer reporter.Close()
+
 		for {
 			err := reporter.Run(ctx)
 			if err == context.Canceled {
diff --git a/cmds/modules/provisiond/reporter.go b/cmds/modules/provisiond/reporter.go
index bb60d403..d42df339 100644
--- a/cmds/modules/provisiond/reporter.go
+++ b/cmds/modules/provisiond/reporter.go
@@ -43,6 +43,15 @@ func reportBuilder() interface{} {
 	return &Report{}
 }
 
+func ReportChecks(metricsPath string) error {
+	rrd, err := rrd.NewRRDBolt(metricsPath, 5*time.Minute, 24*time.Hour)
+	if err != nil {
+		return errors.Wrap(err, "failed to create metrics database")
+	}
+
+	return rrd.Close()
+}
+
 // NewReporter creates a new capacity reporter
 func NewReporter(metricsPath string, cl zbus.Client, root string) (*Reporter, error) {
 	idMgr := stubs.NewIdentityManagerStub(cl)
@@ -284,6 +293,11 @@ func (r *Reporter) getLastReportTime() (int64, bool, error) {
 	return int64(stored), ok, nil
 }
 
+func (r *Reporter) Close() {
+	_ = r.rrd.Close()
+	_ = r.queue.Close()
+}
+
 // Run runs the reporter
 func (r *Reporter) Run(ctx context.Context) error {
 	// go over all user reservations
diff --git a/docs/architecture/decisions/0004-gateway-over-wireguard.md b/docs/architecture/decisions/0004-gateway-over-wireguard.md
new file mode 100644
index 00000000..a7deef9f
--- /dev/null
+++ b/docs/architecture/decisions/0004-gateway-over-wireguard.md
@@ -0,0 +1,24 @@
+# 1. Record architecture decisions
+
+Date: 2023-03-14
+
+## Status
+
+Accepted
+
+## Context
+
+Support gateway over user private wireguard. This means private workloads can be exposed (over the gateway) without using
+yggdrasil.
+
+Users can accomplish this by adding the gateway node to their network (can also act as an access-point) then while configuring
+the gateway they need to set the network name.
+
+## Decision
+
+- User can provide a private ip to the target (backend) now for gateway workloads
+- User has to include the gateway node into his network
+- Gateway workload has to include network name where the ip lives
+- Backward compatible with old configuration. backend still can be over yggdrail
+
+## Consequences
diff --git a/docs/manual/zmachine/cloud-console.md b/docs/manual/zmachine/cloud-console.md
new file mode 100644
index 00000000..f1e03248
--- /dev/null
+++ b/docs/manual/zmachine/cloud-console.md
@@ -0,0 +1,14 @@
+# Cloud console
+
+- `cloud-console` is a tool to view machine logging and interact with the machine you have deployed
+- It always runs on the machine's private network ip and port number equla to `20000 +last octect` of machine private IP
+- For example if the machine ip is `10.20.2.2/24` this means
+  - `cloud-console` is running on `10.20.2.1:20002`
+- For the cloud-console to run we need to start the cloud-hypervisor with option "--serial pty" instead of tty, this allows us to interact with the vm from another process `cloud-console` in our case
+- To be able to connect to the web console you should first start wireguard to connect to the private network
+
+```
+wg-quick up wireguard.conf
+```
+
+- Then go to your browser with the network router IP `10.20.2.1:20002`
diff --git a/pkg/gateway.go b/pkg/gateway.go
index 2b614696..c5725af0 100644
--- a/pkg/gateway.go
+++ b/pkg/gateway.go
@@ -1,5 +1,7 @@
 package pkg
 
+import "github.com/threefoldtech/zos/pkg/gridtypes/zos"
+
 //go:generate mkdir -p stubs
 
 //go:generate zbusc -module gateway -version 0.0.1 -name manager -package stubs github.com/threefoldtech/zos/pkg+Gateway stubs/gateway_stub.go
@@ -22,8 +24,8 @@ func (m *GatewayMetrics) Nu(service string) (result uint64) {
 }
 
 type Gateway interface {
-	SetNamedProxy(wlID string, prefix string, backends []string, TLSPassthrough bool, twinID uint32) (string, error)
-	SetFQDNProxy(wlID string, fqdn string, backends []string, TLSPassthrough bool) error
+	SetNamedProxy(wlID string, config zos.GatewayNameProxy) (string, error)
+	SetFQDNProxy(wlID string, config zos.GatewayFQDNProxy) error
 	DeleteNamedProxy(wlID string) error
 	Metrics() (GatewayMetrics, error)
 }
diff --git a/pkg/gateway/gateway.go b/pkg/gateway/gateway.go
index cf2a8e6b..f1fade7e 100644
--- a/pkg/gateway/gateway.go
+++ b/pkg/gateway/gateway.go
@@ -3,6 +3,7 @@ package gateway
 import (
 	"context"
 	"fmt"
+	"io"
 	"net"
 	"os"
 	"path/filepath"
@@ -16,6 +17,7 @@ import (
 	"github.com/threefoldtech/substrate-client"
 	"github.com/threefoldtech/zbus"
 	"github.com/threefoldtech/zos/pkg"
+	"github.com/threefoldtech/zos/pkg/cache"
 	"github.com/threefoldtech/zos/pkg/environment"
 	"github.com/threefoldtech/zos/pkg/gridtypes"
 	"github.com/threefoldtech/zos/pkg/gridtypes/zos"
@@ -31,21 +33,28 @@ var (
 
 const (
 	traefikService = "traefik"
-	// letsencrypt email need to customizable by he farmer.
-	letsencryptEmail = "letsencrypt@threefold.tech"
+	// letsEncryptEmail email need to customizable by he farmer.
+	letsEncryptEmail = "letsencrypt@threefold.tech"
 	// certResolver must match the one defined in static config
 	httpCertResolver = "resolver"
 	dnsCertResolver  = "dnsresolver"
 	validationPeriod = 1 * time.Hour
+
+	configDir = "proxy"
+	metaDir   = "traefik"
+	zinitDir  = "zinit"
 )
 
 var (
 	ErrTwinIDMismatch       = fmt.Errorf("twin id mismatch")
 	ErrContractNotReserved  = fmt.Errorf("a name contract with the given name must be reserved first")
 	ErrInvalidContractState = fmt.Errorf("the name contract must be in Created state")
+
+	_ pkg.Gateway = (*gatewayModule)(nil)
 )
 
 type gatewayModule struct {
+	volatile string
 	cl       zbus.Client
 	resolver *net.Resolver
 	sub      substrate.Manager
@@ -53,7 +62,6 @@ type gatewayModule struct {
 	reservedDomains map[string]string
 	domainLock      sync.RWMutex
 
-	proxyConfigPath  string
 	staticConfigPath string
 	binPath          string
 	certScriptPath   string
@@ -160,18 +168,82 @@ func loadDomains(ctx context.Context, dir string) (map[string]string, error) {
 	}
 	return domains, nil
 }
+
+// migration moves config that was on persisted storage
+// to volatile storage.
+// gateway should NOT persist config because on node reboot
+// the gw config files are re-created anyway.
+func migrateFiles(root, volatile string) error {
+	// need to move any files from root/proxy to volatile/proxy
+	source := filepath.Join(root, configDir)
+	dest := filepath.Join(volatile, configDir)
+	entries, err := os.ReadDir(source)
+	if os.IsNotExist(err) {
+		return nil
+	} else if err != nil {
+		return err
+	}
+
+	move := func(source, dest string) error {
+		src, err := os.Open(source)
+		if err != nil {
+			return err
+		}
+		defer src.Close()
+		dst, err := os.Create(dest)
+		if err != nil {
+			return err
+		}
+		defer dst.Close()
+		_, err = io.Copy(dst, src)
+		return err
+	}
+
+	for _, entry := range entries {
+		if entry.IsDir() {
+			continue
+		}
+
+		old := filepath.Join(source, entry.Name())
+		new := filepath.Join(dest, entry.Name())
+		if err := move(old, new); err != nil {
+			log.Error().Err(err).Str("name", entry.Name()).Msg("failed to migrate gw config")
+			continue
+		}
+		if err := os.Remove(old); err != nil {
+			log.Error().Err(err).Str("name", entry.Name()).Msg("failed to remove old gw config")
+		}
+	}
+
+	return nil
+}
+
 func New(ctx context.Context, cl zbus.Client, root string) (pkg.Gateway, error) {
 	// where should service-restart/node-reboot recovery be handled?
-	configPath := filepath.Join(root, "proxy")
-	err := os.MkdirAll(configPath, 0644)
-	if err != nil {
-		return nil, errors.Wrap(err, "couldn't make gateway config dir")
+	volatile, err := cache.VolatileDir("gateway", 10*cache.Megabyte)
+	if err != nil && !os.IsExist(err) {
+		return nil, fmt.Errorf("failed to create gateway cache directory: %w", err)
 	}
 
-	traefikMetadata := filepath.Join(root, "traefik")
-	err = os.MkdirAll(traefikMetadata, 0644)
-	if err != nil {
-		return nil, errors.Wrap(err, "couldn't make traefik metadata directory")
+	// create persisted directories
+	for _, dir := range []string{metaDir} {
+		dir = filepath.Join(root, dir)
+		if err := os.MkdirAll(dir, 0644); err != nil {
+			return nil, errors.Wrapf(err, "failed to create directory '%s'", dir)
+		}
+	}
+
+	// create volatile directories
+	for _, dir := range []string{configDir, zinitDir} {
+		dir = filepath.Join(volatile, dir)
+		if err := os.MkdirAll(dir, 0644); err != nil {
+			return nil, errors.Wrapf(err, "failed to create directory '%s'", dir)
+		}
+	}
+
+	// (migration goes here)
+	if err := migrateFiles(root, volatile); err != nil {
+		log.Error().Err(err).Msg("failed doing gateway config migration")
 	}
 
 	bin, err := ensureTraefikBin(ctx, cl)
@@ -190,11 +262,13 @@ func New(ctx context.Context, cl zbus.Client, root string) (pkg.Gateway, error)
 		return nil, errors.Wrap(err, "failed to create cert script")
 	}
 	staticCfgPath := filepath.Join(root, "traefik.yaml")
-	updated, err := staticConfig(staticCfgPath, root, letsencryptEmail)
+	updated, err := staticConfig(staticCfgPath, root, volatile, letsEncryptEmail)
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to create static config")
 	}
-	r := &net.Resolver{
+
+	// we create the resolver to avoid the cache
+	resolver := &net.Resolver{
 		PreferGo: true,
 		Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
 			d := net.Dialer{
@@ -203,7 +277,8 @@ func New(ctx context.Context, cl zbus.Client, root string) (pkg.Gateway, error)
 			return d.DialContext(ctx, network, "8.8.8.8:53")
 		},
 	}
-	domains, err := loadDomains(ctx, configPath)
+
+	domains, err := loadDomains(ctx, filepath.Join(volatile, configDir))
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to load old domains")
 	}
@@ -214,9 +289,9 @@ func New(ctx context.Context, cl zbus.Client, root string) (pkg.Gateway, error)
 
 	gw := &gatewayModule{
 		cl:               cl,
-		resolver:         r,
+		resolver:         resolver,
 		sub:              sub,
-		proxyConfigPath:  configPath,
+		volatile:         volatile,
 		staticConfigPath: staticCfgPath,
 		certScriptPath:   certScriptPath,
 		binPath:          bin,
@@ -235,15 +310,11 @@ func New(ctx context.Context, cl zbus.Client, root string) (pkg.Gateway, error)
 }
 
 func (g *gatewayModule) getReservedDomain(domain string) (string, bool) {
-	g.domainLock.RLock()
-	defer g.domainLock.RUnlock()
 	v, ok := g.reservedDomains[domain]
 	return v, ok
 }
 
 func (g *gatewayModule) setReservedDomain(domain string, wlID string) {
-	g.domainLock.Lock()
-	defer g.domainLock.Unlock()
 	log.Debug().
 		Str("domain", domain).
 		Str("wlID", wlID).
@@ -252,8 +323,6 @@ func (g *gatewayModule) setReservedDomain(domain string, wlID string) {
 }
 
 func (g *gatewayModule) deleteReservedDomain(domain string) {
-	g.domainLock.Lock()
-	defer g.domainLock.Unlock()
 	log.Debug().
 		Str("domain", domain).
 		Msg("deleting domain")
@@ -261,8 +330,9 @@ func (g *gatewayModule) deleteReservedDomain(domain string) {
 }
 
 func (g *gatewayModule) copyReservedDomain() map[string]string {
-	g.domainLock.RLock()
-	defer g.domainLock.RUnlock()
+	g.domainLock.Lock()
+	defer g.domainLock.Unlock()
+
 	res := make(map[string]string, len(g.reservedDomains))
 	for k, v := range g.reservedDomains {
 		res[k] = v
@@ -288,6 +358,7 @@ func (g *gatewayModule) validateNameContracts() error {
 		return nil
 	}
 	reservedDomains := g.copyReservedDomain()
+
 	for domain, id := range reservedDomains {
 		wlID := gridtypes.WorkloadID(id)
 		twinID, _, _, err := wlID.Parts()
@@ -461,7 +532,7 @@ func (g *gatewayModule) startTraefik(z *zinit.Client) error {
 }
 
 func (g *gatewayModule) configPath(name string) string {
-	return filepath.Join(g.proxyConfigPath, fmt.Sprintf("%s.yaml", name))
+	return filepath.Join(g.volatile, configDir, fmt.Sprintf("%s.yaml", name))
 }
 
 func (g *gatewayModule) validateNameContract(name string, twinID uint32) error {
@@ -493,10 +564,18 @@ func (g *gatewayModule) validateNameContract(name string, twinID uint32) error {
 	return nil
 }
 
-func (g *gatewayModule) SetNamedProxy(wlID string, prefix string, backends []string, TLSPassthrough bool, twinID uint32) (string, error) {
+func (g *gatewayModule) SetNamedProxy(wlID string, config zos.GatewayNameProxy) (string, error) {
 	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
 	defer cancel()
 
+	if len(config.Backends) != 1 {
+		return "", fmt.Errorf("only one backend is supported got '%d'", len(config.Backends))
+	}
+
+	twinID, _, _, err := gridtypes.WorkloadID(wlID).Parts()
+	if err != nil {
+		return "", errors.Wrap(err, "invalid workload id")
+	}
 	cfg, err := g.ensureGateway(ctx, false)
 	if err != nil {
 		return "", err
@@ -504,10 +583,11 @@ func (g *gatewayModule) SetNamedProxy(wlID string, prefix string, backends []str
 	if cfg.Domain == "" {
 		return "", errors.New("node doesn't support name proxy (doesn't have a domain)")
 	}
-	if err := g.validateNameContract(prefix, twinID); err != nil {
+	if err := g.validateNameContract(config.Name, twinID); err != nil {
 		return "", errors.Wrap(err, "failed to verify name contract")
 	}
-	fqdn := fmt.Sprintf("%s.%s", prefix, cfg.Domain)
+
+	fqdn := fmt.Sprintf("%s.%s", config.Name, cfg.Domain)
 
 	gatewayTLSConfig := TlsConfig{
 		CertResolver: dnsCertResolver,
@@ -517,44 +597,98 @@ func (g *gatewayModule) SetNamedProxy(wlID string, prefix string, backends []str
 			},
 		},
 	}
-	if err := g.setupRouting(wlID, fqdn, backends, gatewayTLSConfig, TLSPassthrough); err != nil {
+
+	if err := g.setupRouting(ctx, wlID, fqdn, gatewayTLSConfig, config.GatewayBase); err != nil {
 		return "", err
-	} else {
-		return fqdn, nil
 	}
+
+	return fqdn, nil
 }
 
-func (g *gatewayModule) SetFQDNProxy(wlID string, fqdn string, backends []string, TLSPassthrough bool) error {
+func (g *gatewayModule) SetFQDNProxy(wlID string, config zos.GatewayFQDNProxy) error {
 	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
 	defer cancel()
 
+	if len(config.Backends) != 1 {
+		return fmt.Errorf("only one backend is supported got '%d'", len(config.Backends))
+	}
+
 	cfg, err := g.ensureGateway(ctx, false)
 	if err != nil {
 		return err
 	}
 
-	if cfg.Domain != "" && strings.HasSuffix(fqdn, cfg.Domain) {
+	if cfg.Domain != "" && strings.HasSuffix(config.FQDN, cfg.Domain) {
 		return errors.New("can't create a fqdn workload with a subdomain of the gateway's managed domain")
 	}
-	if err := g.verifyDomainDestination(ctx, cfg, fqdn); err != nil {
+	if err := g.verifyDomainDestination(ctx, cfg, config.FQDN); err != nil {
 		return errors.Wrap(err, "failed to verify domain dns record")
 	}
 	gatewayTLSConfig := TlsConfig{
 		CertResolver: httpCertResolver,
 		Domains: []Domain{
 			{
-				Main: fqdn,
+				Main: config.FQDN,
 			},
 		},
 	}
-	return g.setupRouting(wlID, fqdn, backends, gatewayTLSConfig, TLSPassthrough)
+
+	return g.setupRouting(ctx, wlID, config.FQDN, gatewayTLSConfig, config.GatewayBase)
 }
-func (g *gatewayModule) setupRouting(wlID string, fqdn string, backends []string, tlsConfig TlsConfig, TLSPassthrough bool) error {
+
+func (g *gatewayModule) setupRouting(ctx context.Context, wlID string, fqdn string, tlsConfig TlsConfig, config zos.GatewayBase) error {
+	g.domainLock.Lock()
+	defer g.domainLock.Unlock()
+
+	backend := config.Backends[0]
+
+	if err := zos.Backend(backend).Valid(config.TLSPassthrough); err != nil {
+		return errors.Wrapf(err, "failed to validate backend '%s'", backend)
+	}
+
 	if _, ok := g.getReservedDomain(fqdn); ok {
 		return errors.New("domain already registered")
 	}
+
+	if config.Network == nil {
+		// not going over user private network
+		return g.setupRoutingGeneric(wlID, fqdn, tlsConfig, config)
+	}
+
+	// otherwise we need to configure a nnc process
+	// to forward the user traffic.
+
+	// first validate that network exist and get the network namespace
+	twinID, _, _, err := gridtypes.WorkloadID(wlID).Parts()
+	if err != nil {
+		return errors.Wrap(err, "invalid workload id")
+	}
+	// if network is set, means this ip need to be reached from inside the user NR
+	net := stubs.NewNetworkerStub(g.cl)
+	netID := zos.NetworkID(twinID, *config.Network)
+	if _, err := net.GetNet(ctx, netID); err != nil {
+		return errors.Wrap(err, "failed to get user network")
+	}
+	ns := net.Namespace(ctx, netID)
+	backend, err = g.nncEnsure(wlID, ns, config.Backends[0])
+	if err != nil {
+		return errors.Wrap(err, "failed to ensure local gateway")
+	}
+
+	if !config.TLSPassthrough {
+		// if tls passthrough is disabled traefik expecting backend
+		// to be in the format http://<ip>:port
+		backend = zos.Backend(fmt.Sprintf("http://%s", backend))
+	}
+
+	config.Backends = []zos.Backend{backend}
+	return g.setupRoutingGeneric(wlID, fqdn, tlsConfig, config)
+}
+
+func (g *gatewayModule) setupRoutingGeneric(wlID string, fqdn string, tlsConfig TlsConfig, config zos.GatewayBase) error {
+	backend := config.Backends[0]
 	var rule string
-	if TLSPassthrough {
+	if config.TLSPassthrough {
 		rule = fmt.Sprintf("HostSNI(`%s`)", fqdn)
 		tlsConfig = TlsConfig{
 			Passthrough: "true",
@@ -562,17 +696,14 @@ func (g *gatewayModule) setupRouting(wlID string, fqdn string, backends []string
 	} else {
 		rule = fmt.Sprintf("Host(`%s`)", fqdn)
 	}
-	servers := make([]Server, len(backends))
-	for idx, backend := range backends {
-		if err := zos.Backend(backend).Valid(TLSPassthrough); err != nil {
-			return errors.Wrapf(err, "failed to validate backend '%s'", backend)
-		}
-		if TLSPassthrough {
-			servers[idx] = Server{Address: backend}
-		} else {
-			servers[idx] = Server{Url: backend}
-		}
+
+	var server Server
+	if config.TLSPassthrough {
+		server = Server{Address: string(backend)}
+	} else {
+		server = Server{Url: string(backend)}
 	}
+
 	route := fmt.Sprintf("%s-route", wlID)
 	proxyConfig := ProxyConfig{}
 
@@ -587,12 +718,12 @@ func (g *gatewayModule) setupRouting(wlID string, fqdn string, backends []string
 		Services: map[string]Service{
 			wlID: {
 				LoadBalancer: LoadBalancer{
-					Servers: servers,
+					Servers: []Server{server},
 				},
 			},
 		},
 	}
-	if TLSPassthrough {
+	if config.TLSPassthrough {
 		proxyConfig.TCP = routingconfig
 	} else {
 		proxyConfig.Http = routingconfig
@@ -610,6 +741,8 @@ func (g *gatewayModule) setupRouting(wlID string, fqdn string, backends []string
 }
 
 func (g *gatewayModule) DeleteNamedProxy(wlID string) error {
+	g.destroyNNC(wlID)
+
 	path := g.configPath(wlID)
 	_, domain, err := domainFromConfig(path)
 	if os.IsNotExist(err) {
diff --git a/pkg/gateway/nnc.go b/pkg/gateway/nnc.go
new file mode 100644
index 00000000..7cc8c241
--- /dev/null
+++ b/pkg/gateway/nnc.go
@@ -0,0 +1,239 @@
+package gateway
+
+import (
+	"fmt"
+	"math"
+	"math/rand"
+	"net"
+	"os"
+	"path/filepath"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/google/shlex"
+	"github.com/pkg/errors"
+	"github.com/threefoldtech/zos/pkg/gridtypes"
+	"github.com/threefoldtech/zos/pkg/gridtypes/zos"
+	"github.com/threefoldtech/zos/pkg/zinit"
+	"gopkg.in/yaml.v2"
+)
+
+const (
+	nncServicePrefix = "nnc-"
+	nncStartPort     = 2000
+)
+
+// NNC holds (and extracts) information from nnc command
+type NNC struct {
+	ID   gridtypes.WorkloadID
+	Exec string
+}
+
+func (n *NNC) arg(key string) (string, error) {
+	parts, err := shlex.Split(n.Exec)
+	if err != nil {
+		return "", err
+	}
+
+	for i, part := range parts {
+		if part == key {
+			return parts[i+1], nil
+		}
+	}
+
+	return "", fmt.Errorf("not found")
+}
+
+// port return port number this nnc instance is listening on
+func (n *NNC) port() (uint16, error) {
+	listen, err := n.arg("--listen")
+	if err != nil {
+		return 0, err
+	}
+
+	_, port, err := net.SplitHostPort(listen)
+	if err != nil {
+		return 0, err
+	}
+
+	value, err := strconv.Atoi(port)
+	if err != nil {
+		return 0, err
+	}
+
+	return uint16(value), nil
+}
+
+// nncZinitPath return path to the nnc zinit config file given the name
+func (g *gatewayModule) nncZinitPath(name string) string {
+	return filepath.Join(g.volatile, zinitDir, fmt.Sprintf("%s.yaml", name))
+}
+
+// nncList lists all running instances of nnc. the map key is
+// the instance configured listening port
+func (g *gatewayModule) nncList() (map[uint16]NNC, error) {
+	cl := zinit.Default()
+	services, err := cl.List()
+	if err != nil {
+		return nil, err
+	}
+	nncs := make(map[uint16]NNC)
+	// note: should we just instead list the config
+	// from the known config directory?
+	for name := range services {
+		if !strings.HasPrefix(name, nncServicePrefix) {
+			continue
+		}
+		id := strings.TrimPrefix(name, nncServicePrefix)
+
+		cfg, err := cl.Get(name)
+		if err != nil {
+			return nil, err
+		}
+
+		nnc := NNC{
+			ID:   gridtypes.WorkloadID(id),
+			Exec: cfg.Exec,
+		}
+
+		port, err := nnc.port()
+		if err != nil {
+			return nil, err
+		}
+
+		nncs[port] = nnc
+	}
+
+	return nncs, nil
+}
+
+// nncGet return NNC instance by name
+func (g *gatewayModule) nncGet(name string) (NNC, error) {
+	cl := zinit.Default()
+	cfg, err := cl.Get(name)
+	if err != nil {
+		return NNC{}, err
+	}
+
+	return NNC{
+		ID:   gridtypes.WorkloadID(strings.TrimPrefix(name, nncServicePrefix)),
+		Exec: cfg.Exec,
+	}, nil
+
+}
+
+func (g *gatewayModule) nncName(id string) string {
+	return fmt.Sprintf("%s%s", nncServicePrefix, id)
+}
+
+func (g *gatewayModule) nncFreePort() (uint16, error) {
+	// TODO: this need to call while holding some lock
+	// to avoid double allocation of the same port
+	current, err := g.nncList()
+	if err != nil {
+		return 0, err
+	}
+
+	for {
+		port := uint16(rand.Intn(math.MaxUint16-nncStartPort) + nncStartPort)
+		if _, ok := current[port]; !ok {
+			return port, nil
+		}
+	}
+}
+
+func (g *gatewayModule) nncCreateService(name string, service zinit.InitService) error {
+	path := g.nncZinitPath(name)
+	data, err := yaml.Marshal(service)
+	if err != nil {
+		return err
+	}
+
+	if err := os.WriteFile(path, data, 0644); err != nil {
+		return errors.Wrap(err, "failed to create nnc service file")
+	}
+
+	// link under zinit /etc/zinit
+
+	if err := os.Symlink(path, filepath.Join("/etc/zinit", fmt.Sprintf("%s.yaml", name))); err != nil {
+		return errors.Wrap(err, "failed to create nnc service symlink")
+	}
+
+	return nil
+}
+
+// nncEnsure creates (or reuse) an nnc instance given the workload ID. the destination namespace and backend
+// it return the backend that need to be configured in traefik.
+func (g *gatewayModule) nncEnsure(wlID, namespace string, backend zos.Backend) (zos.Backend, error) {
+	name := g.nncName(wlID)
+
+	// reuse or find a new free IP
+	var free uint16
+	nnc, err := g.nncGet(name)
+	if err == nil {
+		// a service with the same name already exists
+		// verify the backend?
+		free, err = nnc.port()
+		// we always destroy the service if
+		// a one already exists with the same name
+		// to allow updating the gw code.
+		g.destroyNNC(wlID)
+	} else if errors.Is(err, zinit.ErrUnknownService) {
+		// if does not exist, just find a free port
+		free, err = g.nncFreePort()
+	} else if err != nil {
+		return "", err
+	}
+
+	// this checks the error returned by the
+	// free port allocation
+	if err != nil {
+		return "", err
+	}
+
+	target, err := backend.AsAddress()
+	if err != nil {
+		return "", err
+	}
+
+	be := zos.Backend(fmt.Sprintf("127.0.0.1:%d", free))
+
+	cmd := []string{
+		"ip", "netns", "exec", "public",
+		"nnc",
+		"--listen", string(be),
+		"--namespace", filepath.Join("/var/run/netns/", namespace),
+		"--target", target,
+	}
+
+	service := zinit.InitService{
+		Exec: strings.Join(cmd, " "),
+	}
+
+	if err = g.nncCreateService(name, service); err != nil {
+		return "", err
+	}
+
+	defer func() {
+		if err != nil {
+			g.destroyNNC(wlID)
+		}
+	}()
+
+	if err = zinit.Default().Monitor(name); err != nil {
+		return "", errors.Wrap(err, "failed to start nnc service")
+	}
+
+	return be, nil
+}
+
+// destroyNNC stops and clean up nnc instances
+func (g *gatewayModule) destroyNNC(wlID string) {
+	name := g.nncName(wlID)
+	path := g.nncZinitPath(name)
+
+	cl := zinit.Default()
+	_ = cl.Destroy(10*time.Second, name)
+	_ = os.Remove(path)
+}
diff --git a/pkg/gateway/static.go b/pkg/gateway/static.go
index ad9594a0..c57e5ea5 100644
--- a/pkg/gateway/static.go
+++ b/pkg/gateway/static.go
@@ -19,8 +19,8 @@ var dConfig string
 var certScript string
 
 // staticConfig write static config to file
-func staticConfig(p, root, email string) (bool, error) {
-	config := fmt.Sprintf(config, root, email)
+func staticConfig(p, root, volatile, email string) (bool, error) {
+	config := fmt.Sprintf(config, root, email, volatile)
 
 	var update bool
 	if oldConfig, err := os.ReadFile(p); os.IsNotExist(err) {
diff --git a/pkg/gateway/static/config.yaml b/pkg/gateway/static/config.yaml
index 894666d6..373b76ff 100644
--- a/pkg/gateway/static/config.yaml
+++ b/pkg/gateway/static/config.yaml
@@ -16,7 +16,7 @@ metrics:
     entryPoint: metrics
 providers:
   file:
-    directory: "%[1]s/proxy"
+    directory: "%[3]s/proxy"
     watch: true
 certificatesResolvers:
   resolver:
@@ -31,4 +31,4 @@ certificatesResolvers:
       email: "%[2]s"
       storage: "%[1]s/traefik/acme2.json"
       dnsChallenge:
-        provider: exec
\ No newline at end of file
+        provider: exec
diff --git a/pkg/gridtypes/zos/gw.go b/pkg/gridtypes/zos/gw.go
new file mode 100644
index 00000000..c1a82b90
--- /dev/null
+++ b/pkg/gridtypes/zos/gw.go
@@ -0,0 +1,139 @@
+package zos
+
+import (
+	"fmt"
+	"io"
+	"math"
+	"net"
+	"net/url"
+	"strconv"
+
+	"github.com/pkg/errors"
+	"github.com/threefoldtech/zos/pkg/gridtypes"
+)
+
+type Backend string
+
+// Parse accepts http://ip:port, http://ip or ip:port
+// checks if backend string is a valid string based on the tlsPassthrough parameter
+// ip:port is only valid in case of tlsPassthrough is true
+// http://ip:port or http://ip is valid in case of tlsPassthrough is false
+func (b Backend) Valid(tlsPassthrough bool) error {
+	if tlsPassthrough {
+		_, _, err := asIpPort(string(b))
+		if err != nil {
+			return fmt.Errorf("failed to parse backend %s with error: %w", b, err)
+		}
+
+	} else {
+		u, err := url.Parse(string(b))
+		if err != nil {
+			return fmt.Errorf("failed to parse backend with error: %w", err)
+		}
+
+		if u.Scheme != "http" {
+			return fmt.Errorf("scheme expected to be http")
+		}
+
+		ip := net.ParseIP(u.Hostname())
+		if len(ip) == 0 || ip.IsLoopback() {
+			return fmt.Errorf("invalid ip address in backend: %s", u.Hostname())
+		}
+	}
+
+	return nil
+}
+
+func asIpPort(a string) (ip net.IP, port uint16, err error) {
+	h, p, err := net.SplitHostPort(a)
+	if err != nil {
+		return nil, 0, fmt.Errorf("failed to parse backend %s with error: %w", a, err)
+	}
+
+	if ip = net.ParseIP(h); ip == nil {
+		return nil, 0, fmt.Errorf("invalid ip")
+	}
+
+	parsedPort, err := strconv.ParseUint(p, 10, 64)
+	if err != nil {
+		return nil, 0, fmt.Errorf("invalid port in backend: %s", p)
+	}
+
+	if parsedPort > math.MaxUint16 {
+		return nil, 0, fmt.Errorf("port '%s' must be <= 65535", p)
+	}
+
+	port = uint16(parsedPort)
+	return
+}
+
+func (b Backend) AsAddress() (string, error) {
+	if _, _, err := asIpPort(string(b)); err == nil {
+		return string(b), nil
+	}
+
+	// otherwise it must be a url
+	u, err := url.Parse(string(b))
+	if err != nil {
+		return "", errors.Wrap(err, "failed to parse backend as a url")
+	}
+
+	if u.Port() == "" {
+		return fmt.Sprintf("%s:80", u.Host), nil
+	}
+
+	return u.Host, nil
+}
+
+// GatewayBase definition. this will proxy name.<zos.domain> to backends
+type GatewayBase struct {
+	// Passthrough whether to pass tls traffic or not
+	TLSPassthrough bool `json:"tls_passthrough"`
+
+	// Backends are list of backend ips (only one is supported atm)
+	Backends []Backend `json:"backends"`
+
+	// Network name to join [optional].
+	// If set the backend IP can be a private ip in that network.
+	// the network then must be
+	// the same rules for tls-passthrough applies.
+	Network *gridtypes.Name `json:"network,omitempty"`
+}
+
+func (g GatewayBase) Valid(getter gridtypes.WorkloadGetter) error {
+	if len(g.Backends) == 0 {
+		return fmt.Errorf("backends list can not be empty")
+	}
+
+	if len(g.Backends) != 1 {
+		return fmt.Errorf("only one backend is supported")
+	}
+
+	for _, backend := range g.Backends {
+		if err := backend.Valid(g.TLSPassthrough); err != nil {
+			return errors.Wrapf(err, "failed to validate backend '%s'", backend)
+		}
+	}
+
+	return nil
+}
+
+func (g GatewayBase) Challenge(w io.Writer) error {
+	if _, err := fmt.Fprintf(w, "%t", g.TLSPassthrough); err != nil {
+		return err
+	}
+
+	for _, backend := range g.Backends {
+		if _, err := fmt.Fprintf(w, "%s", string(backend)); err != nil {
+			return err
+		}
+	}
+
+	if g.Network != nil {
+		if _, err := fmt.Fprintf(w, "%s", *g.Network); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
diff --git a/pkg/gridtypes/zos/gw_fqdn.go b/pkg/gridtypes/zos/gw_fqdn.go
index 080cee26..33fed4c0 100644
--- a/pkg/gridtypes/zos/gw_fqdn.go
+++ b/pkg/gridtypes/zos/gw_fqdn.go
@@ -3,13 +3,8 @@ package zos
 import (
 	"fmt"
 	"io"
-	"math"
-	"net"
-	"net/url"
 	"regexp"
-	"strconv"
 
-	"github.com/pkg/errors"
 	"github.com/threefoldtech/zos/pkg/gridtypes"
 )
 
@@ -17,59 +12,12 @@ var (
 	gwNameRegex = regexp.MustCompile(`^[a-zA-Z0-9-.]+$`)
 )
 
-type Backend string
-
-// Parse accepts http://ip:port, http://ip or ip:port
-// checks if backend string is a valid string based on the tlsPassthrough parameter
-// ip:port is only valid in case of tlsPassthrough is true
-// http://ip:port or http://ip is valid in case of tlsPassthrough is false
-func (b Backend) Valid(tlsPassthrough bool) error {
-	var hostName string
-	if tlsPassthrough {
-		host, port, err := net.SplitHostPort(string(b))
-		if err != nil {
-			return fmt.Errorf("failed to parse backend %s with error: %w", b, err)
-		}
-
-		parsedPort, err := strconv.ParseUint(port, 10, 64)
-		if err != nil {
-			return fmt.Errorf("invalid port in backend: %s", port)
-		}
-
-		if parsedPort > math.MaxUint16 {
-			return fmt.Errorf("port '%s' must be <= 65535", port)
-		}
-
-		hostName = host
-	} else {
-		u, err := url.Parse(string(b))
-		if err != nil {
-			return fmt.Errorf("failed to parse backend with error: %w", err)
-		}
-
-		if u.Scheme != "http" {
-			return fmt.Errorf("scheme expected to be http")
-		}
-		hostName = u.Hostname()
-	}
-
-	ip := net.ParseIP(hostName)
-	if len(ip) == 0 || ip.IsLoopback() {
-		return fmt.Errorf("invalid ip address in backend: %s", hostName)
-	}
-	return nil
-}
-
 // GatewayFQDNProxy definition. this will proxy name.<zos.domain> to backends
 type GatewayFQDNProxy struct {
+	GatewayBase
+
 	// FQDN the fully qualified domain name to use (cannot be present with Name)
 	FQDN string `json:"fqdn"`
-
-	// Passthroug whether to pass tls traffic or not
-	TLSPassthrough bool `json:"tls_passthrough"`
-
-	// Backends are list of backend ips
-	Backends []Backend `json:"backends"`
 }
 
 func (g GatewayFQDNProxy) Valid(getter gridtypes.WorkloadGetter) error {
@@ -79,16 +27,8 @@ func (g GatewayFQDNProxy) Valid(getter gridtypes.WorkloadGetter) error {
 	if g.FQDN[len(g.FQDN)-1] == '.' {
 		return fmt.Errorf("fqdn can't end with a dot")
 	}
-	if len(g.Backends) == 0 {
-		return fmt.Errorf("backends list can not be empty")
-	}
-	for _, backend := range g.Backends {
-		if err := backend.Valid(g.TLSPassthrough); err != nil {
-			return errors.Wrapf(err, "failed to validate backend '%s'", backend)
-		}
-	}
 
-	return nil
+	return g.GatewayBase.Valid(getter)
 }
 
 func (g GatewayFQDNProxy) Challenge(w io.Writer) error {
@@ -96,17 +36,7 @@ func (g GatewayFQDNProxy) Challenge(w io.Writer) error {
 		return err
 	}
 
-	if _, err := fmt.Fprintf(w, "%t", g.TLSPassthrough); err != nil {
-		return err
-	}
-
-	for _, backend := range g.Backends {
-		if _, err := fmt.Fprintf(w, "%s", string(backend)); err != nil {
-			return err
-		}
-	}
-
-	return nil
+	return g.GatewayBase.Challenge(w)
 }
 
 func (g GatewayFQDNProxy) Capacity() (gridtypes.Capacity, error) {
@@ -117,6 +47,5 @@ func (g GatewayFQDNProxy) Capacity() (gridtypes.Capacity, error) {
 }
 
 // GatewayProxyResult results
-type GatewayProxyResult struct {
-	FQDN string `json:"fqdn"`
+type GatewayFQDNResult struct {
 }
diff --git a/pkg/gridtypes/zos/gw_name.go b/pkg/gridtypes/zos/gw_name.go
index c5ac1137..6a272365 100644
--- a/pkg/gridtypes/zos/gw_name.go
+++ b/pkg/gridtypes/zos/gw_name.go
@@ -4,36 +4,22 @@ import (
 	"fmt"
 	"io"
 
-	"github.com/pkg/errors"
 	"github.com/threefoldtech/zos/pkg/gridtypes"
 )
 
 // GatewayNameProxy definition. this will proxy name.<zos.domain> to backends
 type GatewayNameProxy struct {
+	GatewayBase
 	// Name the fully qualified domain name to use (cannot be present with Name)
 	Name string `json:"name"`
-
-	// Passthrough whether to pass tls traffic or not
-	TLSPassthrough bool `json:"tls_passthrough"`
-
-	// Backends are list of backend ips
-	Backends []Backend `json:"backends"`
 }
 
 func (g GatewayNameProxy) Valid(getter gridtypes.WorkloadGetter) error {
 	if !gwNameRegex.MatchString(g.Name) {
 		return fmt.Errorf("invalid name")
 	}
-	if len(g.Backends) == 0 {
-		return fmt.Errorf("backends list can not be empty")
-	}
-	for _, backend := range g.Backends {
-		if err := backend.Valid(g.TLSPassthrough); err != nil {
-			return errors.Wrapf(err, "failed to validate backend '%s'", backend)
-		}
-	}
 
-	return nil
+	return g.GatewayBase.Valid(getter)
 }
 
 func (g GatewayNameProxy) Challenge(w io.Writer) error {
@@ -41,17 +27,7 @@ func (g GatewayNameProxy) Challenge(w io.Writer) error {
 		return err
 	}
 
-	if _, err := fmt.Fprintf(w, "%t", g.TLSPassthrough); err != nil {
-		return err
-	}
-
-	for _, backend := range g.Backends {
-		if _, err := fmt.Fprintf(w, "%s", string(backend)); err != nil {
-			return err
-		}
-	}
-
-	return nil
+	return g.GatewayBase.Challenge(w)
 }
 
 func (g GatewayNameProxy) Capacity() (gridtypes.Capacity, error) {
@@ -62,5 +38,6 @@ func (g GatewayNameProxy) Capacity() (gridtypes.Capacity, error) {
 }
 
 // GatewayProxyResult results
-type GatewayFQDNResult struct {
+type GatewayProxyResult struct {
+	FQDN string `json:"fqdn"`
 }
diff --git a/pkg/gridtypes/zos/zmachine.go b/pkg/gridtypes/zos/zmachine.go
index e38c9e7b..f695b58d 100644
--- a/pkg/gridtypes/zos/zmachine.go
+++ b/pkg/gridtypes/zos/zmachine.go
@@ -271,7 +271,8 @@ func (v ZMachine) Challenge(b io.Writer) error {
 
 // ZMachineResult result returned by VM reservation
 type ZMachineResult struct {
-	ID    string `json:"id"`
-	IP    string `json:"ip"`
-	YggIP string `json:"ygg_ip"`
+	ID         string  `json:"id"`
+	IP         string  `json:"ip"`
+	YggIP      string  `json:"ygg_ip"`
+	ConsoleURL string `json:"console_url"`
 }
diff --git a/pkg/primitives/gateway/gatewayfqdn.go b/pkg/primitives/gateway/gatewayfqdn.go
index d59be448..3b3b4824 100644
--- a/pkg/primitives/gateway/gatewayfqdn.go
+++ b/pkg/primitives/gateway/gatewayfqdn.go
@@ -31,12 +31,9 @@ func (p *FQDNManager) Provision(ctx context.Context, wl *gridtypes.WorkloadWithI
 	if err := json.Unmarshal(wl.Data, &proxy); err != nil {
 		return nil, fmt.Errorf("failed to unmarshal gateway proxy from reservation: %w", err)
 	}
-	backends := make([]string, len(proxy.Backends))
-	for idx, backend := range proxy.Backends {
-		backends[idx] = string(backend)
-	}
+
 	gateway := stubs.NewGatewayStub(p.zbus)
-	err := gateway.SetFQDNProxy(ctx, wl.ID.String(), proxy.FQDN, backends, proxy.TLSPassthrough)
+	err := gateway.SetFQDNProxy(ctx, wl.ID.String(), proxy)
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to setup fqdn proxy")
 	}
diff --git a/pkg/primitives/gateway/gatewayname.go b/pkg/primitives/gateway/gatewayname.go
index d97fa697..04e84876 100644
--- a/pkg/primitives/gateway/gatewayname.go
+++ b/pkg/primitives/gateway/gatewayname.go
@@ -33,13 +33,9 @@ func (p *NameManager) Provision(ctx context.Context, wl *gridtypes.WorkloadWithI
 	if err := json.Unmarshal(wl.Data, &proxy); err != nil {
 		return nil, fmt.Errorf("failed to unmarshal gateway proxy from reservation: %w", err)
 	}
-	backends := make([]string, len(proxy.Backends))
-	for idx, backend := range proxy.Backends {
-		backends[idx] = string(backend)
-	}
-	twinID, _ := provision.GetDeploymentID(ctx)
+
 	gateway := stubs.NewGatewayStub(p.zbus)
-	fqdn, err := gateway.SetNamedProxy(ctx, wl.ID.String(), proxy.Name, backends, proxy.TLSPassthrough, twinID)
+	fqdn, err := gateway.SetNamedProxy(ctx, wl.ID.String(), proxy)
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to setup name proxy")
 	}
diff --git a/pkg/primitives/vm/utils.go b/pkg/primitives/vm/utils.go
index 45ba5a27..ada16477 100644
--- a/pkg/primitives/vm/utils.go
+++ b/pkg/primitives/vm/utils.go
@@ -115,6 +115,7 @@ func (p *Manager) newPrivNetworkInterface(ctx context.Context, dl gridtypes.Depl
 		IP6DefaultGateway: gw6,
 		PublicIPv4:        false,
 		PublicIPv6:        false,
+		NetID:             netID,
 	}
 
 	return out, nil
diff --git a/pkg/primitives/vm/vm.go b/pkg/primitives/vm/vm.go
index 4214098b..2e094ee3 100644
--- a/pkg/primitives/vm/vm.go
+++ b/pkg/primitives/vm/vm.go
@@ -352,12 +352,13 @@ func (p *Manager) virtualMachineProvisionImpl(ctx context.Context, wl *gridtypes
 	machine.Environment = config.Env
 	machine.Hostname = wl.Name.String()
 
-	if err = vm.Run(ctx, machine); err != nil {
+	machineInfo, err := vm.Run(ctx, machine)
+	if err != nil {
 		// attempt to delete the vm, should the process still be lingering
 		log.Error().Err(err).Msg("cleaning up vm deployment duo to an error")
 		_ = vm.Delete(ctx, wl.ID.String())
 	}
-
+	result.ConsoleURL = machineInfo.ConsoleURL
 	return result, err
 }
 func (p *Manager) copyFile(srcPath string, destPath string, permissions os.FileMode) error {
diff --git a/pkg/rrd/rrd.go b/pkg/rrd/rrd.go
index 42e76fb9..797e3804 100644
--- a/pkg/rrd/rrd.go
+++ b/pkg/rrd/rrd.go
@@ -23,6 +23,9 @@ type RRD interface {
 	// Last returns the last reported value for a metric given the metric
 	// name
 	Last(key string) (value float64, ok bool, err error)
+
+	// Close the db
+	Close() error
 }
 
 type Printer interface {
@@ -86,6 +89,10 @@ func newRRDBolt(path string, window time.Duration, retention time.Duration) (*rr
 	}, nil
 }
 
+func (r *rrdBolt) Close() error {
+	return r.db.Close()
+}
+
 func (r *rrdBolt) printBucket(bucket *bolt.Bucket, out io.Writer) error {
 	cur := bucket.Cursor()
 	for k, v := cur.First(); k != nil; k, v = cur.Next() {
diff --git a/pkg/stubs/gateway_stub.go b/pkg/stubs/gateway_stub.go
index f7b431fc..18b8209f 100644
--- a/pkg/stubs/gateway_stub.go
+++ b/pkg/stubs/gateway_stub.go
@@ -8,6 +8,7 @@ import (
 	"context"
 	zbus "github.com/threefoldtech/zbus"
 	pkg "github.com/threefoldtech/zos/pkg"
+	zos "github.com/threefoldtech/zos/pkg/gridtypes/zos"
 )
 
 type GatewayStub struct {
@@ -59,8 +60,8 @@ func (s *GatewayStub) Metrics(ctx context.Context) (ret0 pkg.GatewayMetrics, ret
 	return
 }
 
-func (s *GatewayStub) SetFQDNProxy(ctx context.Context, arg0 string, arg1 string, arg2 []string, arg3 bool) (ret0 error) {
-	args := []interface{}{arg0, arg1, arg2, arg3}
+func (s *GatewayStub) SetFQDNProxy(ctx context.Context, arg0 string, arg1 zos.GatewayFQDNProxy) (ret0 error) {
+	args := []interface{}{arg0, arg1}
 	result, err := s.client.RequestContext(ctx, s.module, s.object, "SetFQDNProxy", args...)
 	if err != nil {
 		panic(err)
@@ -74,8 +75,8 @@ func (s *GatewayStub) SetFQDNProxy(ctx context.Context, arg0 string, arg1 string
 	return
 }
 
-func (s *GatewayStub) SetNamedProxy(ctx context.Context, arg0 string, arg1 string, arg2 []string, arg3 bool, arg4 uint32) (ret0 string, ret1 error) {
-	args := []interface{}{arg0, arg1, arg2, arg3, arg4}
+func (s *GatewayStub) SetNamedProxy(ctx context.Context, arg0 string, arg1 zos.GatewayNameProxy) (ret0 string, ret1 error) {
+	args := []interface{}{arg0, arg1}
 	result, err := s.client.RequestContext(ctx, s.module, s.object, "SetNamedProxy", args...)
 	if err != nil {
 		panic(err)
diff --git a/pkg/stubs/vmd_stub.go b/pkg/stubs/vmd_stub.go
index 7ff9c126..b03fa840 100644
--- a/pkg/stubs/vmd_stub.go
+++ b/pkg/stubs/vmd_stub.go
@@ -141,15 +141,17 @@ func (s *VMModuleStub) Metrics(ctx context.Context) (ret0 pkg.MachineMetrics, re
 	return
 }
 
-func (s *VMModuleStub) Run(ctx context.Context, arg0 pkg.VM) (ret0 error) {
+func (s *VMModuleStub) Run(ctx context.Context, arg0 pkg.VM) (ret0 pkg.MachineInfo, ret1 error) {
 	args := []interface{}{arg0}
 	result, err := s.client.RequestContext(ctx, s.module, s.object, "Run", args...)
 	if err != nil {
 		panic(err)
 	}
 	result.PanicOnError()
-	ret0 = result.CallError()
-	loader := zbus.Loader{}
+	ret1 = result.CallError()
+	loader := zbus.Loader{
+		&ret0,
+	}
 	if err := result.Unmarshal(&loader); err != nil {
 		panic(err)
 	}
diff --git a/pkg/vm.go b/pkg/vm.go
index 60847299..f9ccc4ee 100644
--- a/pkg/vm.go
+++ b/pkg/vm.go
@@ -7,6 +7,7 @@ import (
 	"path/filepath"
 
 	"github.com/threefoldtech/zos/pkg/gridtypes"
+	"github.com/threefoldtech/zos/pkg/gridtypes/zos"
 )
 
 //go:generate zbusc -module vmd -version 0.0.1 -name manager -package stubs github.com/threefoldtech/zos/pkg+VMModule stubs/vmd_stub.go
@@ -37,6 +38,8 @@ type VMIface struct {
 	PublicIPv4 bool
 	// PublicIPv4 holds a public Ipv6
 	PublicIPv6 bool
+	// NetId holds network id (for private network only)
+	NetID zos.NetID
 }
 
 // VMNetworkInfo structure
@@ -230,6 +233,9 @@ type MachineMetric struct {
 	Private NetMetric
 	Public  NetMetric
 }
+type MachineInfo struct {
+	ConsoleURL string
+}
 
 // MachineMetrics container for metrics from multiple machines
 type MachineMetrics map[string]MachineMetric
@@ -254,7 +260,7 @@ func (s *Stream) Valid() error {
 
 // VMModule defines the virtual machine module interface
 type VMModule interface {
-	Run(vm VM) error
+	Run(vm VM) (MachineInfo, error)
 	Inspect(name string) (VMInfo, error)
 	Delete(name string) error
 	Exists(name string) bool
diff --git a/pkg/vm/ch.go b/pkg/vm/ch.go
index 636490f2..1c3d1036 100644
--- a/pkg/vm/ch.go
+++ b/pkg/vm/ch.go
@@ -5,23 +5,65 @@ import (
 	"context"
 	"fmt"
 	"io"
+	"math"
+	"net"
 	"os"
 	"os/exec"
 	"path/filepath"
 	"strings"
 	"syscall"
+	"time"
 
+	"github.com/cenkalti/backoff/v3"
 	"github.com/google/shlex"
 	"github.com/pkg/errors"
 	"github.com/rs/zerolog/log"
+	"github.com/threefoldtech/zos/pkg"
 )
 
 const (
-	chBin = "cloud-hypervisor"
+	chBin           = "cloud-hypervisor"
+	cloudConsoleBin = "cloud-console"
 )
 
+// startCloudConsole Starts the cloud console for the vm on it's private network ip
+func (m *Machine) startCloudConsole(ctx context.Context, namespace string, networkAddr net.IPNet, machineIP net.IPNet, ptyPath string) (string, error) {
+	ipv4 := machineIP.IP.To4()
+	if ipv4 == nil {
+		return "", fmt.Errorf("invalid vm ip address (%s) not ipv4", machineIP.IP.String())
+	}
+	port := 20000 + uint16(ipv4[3])
+	if port == math.MaxUint16 {
+		// this should be impossible since a byte max value is 512 hence 20_000 + 512 can never be over
+		// max of uint16
+		return "", fmt.Errorf("couldn't start cloud console port number exceeds %d", port)
+	}
+	args := []string{
+		"setsid",
+		"ip",
+		"netns",
+		"exec", namespace,
+		cloudConsoleBin,
+		ptyPath,
+		networkAddr.IP.String(),
+		fmt.Sprint(port),
+	}
+
+	log.Debug().Msgf("running cloud-console : %+v", args)
+
+	cmd := exec.CommandContext(ctx, "busybox", args...)
+	if err := cmd.Start(); err != nil {
+		return "", errors.Wrap(err, "failed to start cloud-hypervisor")
+	}
+	if err := m.release(cmd.Process); err != nil {
+		return "", err
+	}
+	consoleURL := fmt.Sprintf("%s:%d", networkAddr.IP.String(), port)
+	return consoleURL, nil
+}
+
 // Run run the machine with cloud-hypervisor
-func (m *Machine) Run(ctx context.Context, socket, logs string) error {
+func (m *Machine) Run(ctx context.Context, socket, logs string) (pkg.MachineInfo, error) {
 	_ = os.Remove(socket)
 
 	// build command line
@@ -33,7 +75,7 @@ func (m *Machine) Run(ctx context.Context, socket, logs string) error {
 		"--memory": {fmt.Sprintf("%s,shared=on", m.Config.Mem.String())},
 
 		"--console":    {"off"},
-		"--serial":     {"tty"},
+		"--serial":     {"pty"}, // we use pty here for the cloud console to be able to read the vm console, in case of debuging or we need stdout logging we use tty
 		"--api-socket": {socket},
 	}
 	var err error
@@ -52,7 +94,7 @@ func (m *Machine) Run(ctx context.Context, socket, logs string) error {
 		var pid int
 		pid, err = m.startFs(socket, fs.Path)
 		if err != nil {
-			return err
+			return pkg.MachineInfo{}, err
 		}
 		pids = append(pids, pid)
 		filesystems = append(filesystems, fmt.Sprintf("tag=%s,socket=%s", fs.Tag, socket))
@@ -62,7 +104,7 @@ func (m *Machine) Run(ctx context.Context, socket, logs string) error {
 		args["--fs"] = filesystems
 	}
 	if err := m.buildZosRC(); err != nil {
-		return errors.Wrap(err, "couldn't build zosrc")
+		return pkg.MachineInfo{}, errors.Wrap(err, "couldn't build zosrc")
 	}
 	if m.Boot.Initrd != "" {
 		args["--initramfs"] = []string{m.Boot.Initrd}
@@ -84,13 +126,13 @@ func (m *Machine) Run(ctx context.Context, socket, logs string) error {
 			var typ InterfaceType
 			typ, _, err = nic.getType()
 			if err != nil {
-				return errors.Wrapf(err, "failed to detect interface type '%s'", nic.Tap)
+				return pkg.MachineInfo{}, errors.Wrapf(err, "failed to detect interface type '%s'", nic.Tap)
 			}
 			if typ == InterfaceTAP {
 				interfaces = append(interfaces, nic.asTap())
 			} else {
 				err = fmt.Errorf("unsupported tap device type '%s'", nic.Tap)
-				return err
+				return pkg.MachineInfo{}, err
 			}
 		}
 		args["--net"] = interfaces
@@ -114,7 +156,7 @@ func (m *Machine) Run(ctx context.Context, socket, logs string) error {
 	// to save up storage.
 	logFd, err := os.OpenFile(logs, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
 	if err != nil {
-		return err
+		return pkg.MachineInfo{}, err
 	}
 	defer logFd.Close()
 
@@ -132,7 +174,7 @@ func (m *Machine) Run(ctx context.Context, socket, logs string) error {
 	cmd.Stdout = logFd
 	cmd.Stderr = logFd
 
-	// TODO: always get permission denied when setting
+	// TODO: alMachineInfo{}, ways get permission denied when setting
 	// sid with sys proc attr
 	// cmd.SysProcAttr = &syscall.SysProcAttr{
 	// 	Setsid:     true,
@@ -148,7 +190,7 @@ func (m *Machine) Run(ctx context.Context, socket, logs string) error {
 		var tap *os.File
 		tap, err = os.OpenFile(filepath.Join("/dev", fmt.Sprintf("tap%d", tapindex)), os.O_RDWR, 0600)
 		if err != nil {
-			return err
+			return pkg.MachineInfo{}, err
 		}
 		toClose = append(toClose, tap)
 		cmd.ExtraFiles = append(cmd.ExtraFiles, tap)
@@ -161,13 +203,75 @@ func (m *Machine) Run(ctx context.Context, socket, logs string) error {
 	}()
 
 	if err = cmd.Start(); err != nil {
-		return errors.Wrap(err, "failed to start cloud-hypervisor")
+		return pkg.MachineInfo{}, errors.Wrap(err, "failed to start cloud-hypervisor")
 	}
 
 	if err = m.release(cmd.Process); err != nil {
+		return pkg.MachineInfo{}, err
+	}
+
+	if err := m.waitAndAdjOom(ctx, m.ID, socket); err != nil {
+		return pkg.MachineInfo{}, err
+	}
+	client := NewClient(socket)
+	vmData, err := client.Inspect(ctx)
+
+	if err != nil {
+		return pkg.MachineInfo{}, errors.Wrapf(err, "failed to Inspect vm with id: '%s'", m.ID)
+	}
+	consoleURL := ""
+	for _, ifc := range m.Interfaces {
+		if ifc.Console != nil {
+			consoleURL, err = m.startCloudConsole(ctx, ifc.Console.Namespace, ifc.Console.NetworkAddr, ifc.Console.IP, vmData.PTYPath)
+			if err != nil {
+				log.Error().Err(err).Str("vm", m.ID).Msg("failed to start cloud-console for vm")
+			}
+		}
+	}
+
+	return pkg.MachineInfo{ConsoleURL: consoleURL}, nil
+}
+
+func (m *Machine) waitAndAdjOom(ctx context.Context, name string, socket string) error {
+	check := func() error {
+		if _, err := Find(name); err != nil {
+			return fmt.Errorf("failed to spawn vm machine process '%s'", name)
+		}
+
+		con, err := net.Dial("unix", socket)
+		if err != nil {
+			return err
+		}
+
+		con.Close()
+		return nil
+	}
+
+	ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
+	defer cancel()
+
+	if err := backoff.RetryNotify(
+		check,
+		backoff.WithContext(
+			backoff.NewConstantBackOff(2*time.Second),
+			ctx,
+		),
+		func(err error, d time.Duration) {
+			log.Debug().Err(err).Str("id", name).Msg("vm is not up yet")
+		}); err != nil {
+
 		return err
 	}
 
+	ps, err := Find(name)
+	if err != nil {
+		return errors.Wrapf(err, "failed to find vm with id '%s'", name)
+	}
+
+	if err := os.WriteFile(filepath.Join("/proc/", fmt.Sprint(ps.Pid), "oom_adj"), []byte("-17"), 0644); err != nil {
+		return errors.Wrapf(err, "failed to update oom priority for machine '%s' (PID: %d)", name, ps.Pid)
+	}
+
 	return nil
 }
 
diff --git a/pkg/vm/client.go b/pkg/vm/client.go
index f41896cc..aa142102 100644
--- a/pkg/vm/client.go
+++ b/pkg/vm/client.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"io"
 	"net"
 	"net/http"
 
@@ -15,6 +16,12 @@ type Client struct {
 	client http.Client
 }
 
+type VMData struct {
+	CPU     CPU
+	Memory  MemMib
+	PTYPath string
+}
+
 // NewClient creates a new instance of client
 func NewClient(unix string) *Client {
 	client := Client{
@@ -86,40 +93,48 @@ func (c *Client) Resume(ctx context.Context) error {
 }
 
 // Inspect return information about the vm
-func (c *Client) Inspect(ctx context.Context) (CPU, MemMib, error) {
+func (c *Client) Inspect(ctx context.Context) (VMData, error) {
 	request, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://unix/api/v1/vm.info", nil)
 	if err != nil {
-		return 0, 0, err
+		return VMData{}, err
 	}
 	request.Header.Add("content-type", "application/json")
 
 	response, err := c.client.Do(request)
 	if err != nil {
-		return 0, 0, errors.Wrap(err, "error calling machine info")
+		return VMData{}, errors.Wrap(err, "error calling machine info")
 	}
 
 	defer response.Body.Close()
 
 	if response.StatusCode != http.StatusOK {
-		return 0, 0, fmt.Errorf("got unexpected http code '%s' on machine info", response.Status)
+		body, _ := io.ReadAll(response.Body)
+		response.Body.Close()
+
+		return VMData{}, fmt.Errorf("got unexpected http code '%s' on machine info, Response: %s", response.Status, string(body))
 	}
 
 	var data struct {
 		Config struct {
-			CPUs struct {
+			CPU struct {
 				Boot uint8 `json:"boot_vcpus"`
 			} `json:"cpus"`
 			Memory struct {
 				Size int64 `json:"size"`
 			} `json:"memory"`
+			Serial struct {
+				PTYPath string `json:"file"`
+			} `json:"serial"`
 		} `json:"config"`
 	}
 
 	if err := json.NewDecoder(response.Body).Decode(&data); err != nil {
-		return 0, 0, errors.Wrap(err, "failed to parse machine information")
+		return VMData{}, errors.Wrap(err, "failed to parse machine information")
 	}
-
-	return CPU(data.Config.CPUs.Boot),
-		MemMib(data.Config.Memory.Size / (1024 * 1024)),
-		nil
+	vmData := VMData{
+		CPU:     CPU(data.Config.CPU.Boot),
+		Memory:  MemMib(data.Config.Memory.Size / (1024 * 1024)),
+		PTYPath: data.Config.Serial.PTYPath,
+	}
+	return vmData, nil
 }
diff --git a/pkg/vm/machine.go b/pkg/vm/machine.go
index db7f7787..b3b9daf9 100644
--- a/pkg/vm/machine.go
+++ b/pkg/vm/machine.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"encoding/json"
 	"fmt"
+	"net"
 	"os"
 
 	"github.com/pkg/errors"
@@ -50,11 +51,18 @@ const (
 	InterfaceTAP InterfaceType = "tuntap"
 )
 
+type Console struct {
+	Namespace   string    `json:"namespace"`
+	NetworkAddr net.IPNet `json:"network_addr"`
+	IP          net.IPNet `json:"ip"`
+}
+
 // Interface nic struct
 type Interface struct {
-	ID  string `json:"iface_id"`
-	Tap string `json:"host_dev_name"`
-	Mac string `json:"guest_mac,omitempty"`
+	ID      string   `json:"iface_id"`
+	Tap     string   `json:"host_dev_name"`
+	Mac     string   `json:"guest_mac,omitempty"`
+	Console *Console `json:"console,omitempty"`
 }
 
 // asTap returns the command line argument for this interface as a tap device
diff --git a/pkg/vm/manager.go b/pkg/vm/manager.go
index 000b490c..05581bdf 100644
--- a/pkg/vm/manager.go
+++ b/pkg/vm/manager.go
@@ -4,7 +4,6 @@ import (
 	"context"
 	"fmt"
 	"io"
-	"net"
 	"os"
 	"path/filepath"
 	"strings"
@@ -12,13 +11,13 @@ import (
 	"syscall"
 	"time"
 
-	"github.com/cenkalti/backoff/v3"
 	"github.com/patrickmn/go-cache"
 	"github.com/pkg/errors"
 	"github.com/rs/zerolog/log"
 	"github.com/threefoldtech/zbus"
 	"github.com/threefoldtech/zos/pkg"
 	"github.com/threefoldtech/zos/pkg/gridtypes"
+	"github.com/threefoldtech/zos/pkg/stubs"
 	"github.com/threefoldtech/zos/pkg/vm/cloudinit"
 )
 
@@ -147,7 +146,18 @@ func (m *Module) Exists(id string) bool {
 	return err == nil
 }
 
-func (m *Module) makeNetwork(vm *pkg.VM, cfg *cloudinit.Configuration) ([]Interface, error) {
+func (m *Module) getConsoleConfig(ctx context.Context, vmName string, ifc pkg.VMIface) (*Console, error) {
+	stub := stubs.NewNetworkerStub(m.client)
+	namespace := stub.Namespace(ctx, ifc.NetID)
+
+	networkAddr, err := stub.GetSubnet(ctx, ifc.NetID)
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to get network '%s'", ifc.NetID)
+	}
+	return &Console{Namespace: namespace, NetworkAddr: networkAddr, IP: ifc.IPs[0]}, nil
+
+}
+func (m *Module) makeNetwork(ctx context.Context, vm *pkg.VM, cfg *cloudinit.Configuration) ([]Interface, error) {
 	// assume there is always at least 1 iface present
 
 	// we do 2 things here:
@@ -176,6 +186,14 @@ func (m *Module) makeNetwork(vm *pkg.VM, cfg *cloudinit.Configuration) ([]Interf
 			Tap: ifcfg.Tap,
 			Mac: ifcfg.MAC,
 		}
+		if ifcfg.NetID != "" && len(ifcfg.IPs) > 0 {
+			// if NetID is set on this interface means it is a private network so we add console config to it.
+			console, err := m.getConsoleConfig(ctx, vm.Name, ifcfg)
+			if err != nil {
+				return nil, errors.Wrapf(err, "could not get console config for vm %s", vm.Name)
+			}
+			nic.Console = console
+		}
 		nics = append(nics, nic)
 
 		cinet := cloudinit.Ethernet{
@@ -317,18 +335,18 @@ func (m *Module) List() ([]string, error) {
 }
 
 // Run vm
-func (m *Module) Run(vm pkg.VM) error {
+func (m *Module) Run(vm pkg.VM) (pkg.MachineInfo, error) {
 	m.lock.Lock()
 	defer m.lock.Unlock()
 
 	if err := vm.Validate(); err != nil {
-		return errors.Wrap(err, "machine configuration validation failed")
+		return pkg.MachineInfo{}, errors.Wrap(err, "machine configuration validation failed")
 	}
 
 	ctx := context.Background()
 
 	if m.Exists(vm.Name) {
-		return fmt.Errorf("a vm with same name already exists")
+		return pkg.MachineInfo{}, fmt.Errorf("a vm with same name already exists")
 	}
 
 	cfg := cloudinit.Configuration{
@@ -364,11 +382,11 @@ func (m *Module) Run(vm pkg.VM) error {
 
 	devices, err := m.makeDevices(&vm)
 	if err != nil {
-		return err
+		return pkg.MachineInfo{}, err
 	}
 	fs, err := m.makeVirtioFilesystems(&vm)
 	if err != nil {
-		return errors.Wrap(err, "failed while constructing qsfs filesystems")
+		return pkg.MachineInfo{}, errors.Wrap(err, "failed while constructing qsfs filesystems")
 	}
 
 	cmdline := vm.KernelArgs
@@ -411,15 +429,15 @@ func (m *Module) Run(vm pkg.VM) error {
 		}
 	}
 
-	nics, err := m.makeNetwork(&vm, &cfg)
+	nics, err := m.makeNetwork(ctx, &vm, &cfg)
 	if err != nil {
-		return err
+		return pkg.MachineInfo{}, err
 	}
 
 	ciImage := m.cloudInitImage(vm.Name)
 
 	if err := cloudinit.CreateImage(ciImage, cfg); err != nil {
-		return errors.Wrap(err, "failed to create cloud-init image")
+		return pkg.MachineInfo{}, errors.Wrap(err, "failed to create cloud-init image")
 	}
 
 	devices = append(devices, Disk{
@@ -450,7 +468,7 @@ func (m *Module) Run(vm pkg.VM) error {
 
 	log.Debug().Str("name", vm.Name).Msg("saving machine")
 	if err := machine.Save(m.configPath(vm.Name)); err != nil {
-		return err
+		return pkg.MachineInfo{}, err
 	}
 
 	defer func() {
@@ -464,59 +482,12 @@ func (m *Module) Run(vm pkg.VM) error {
 		m.failures.Set(vm.Name, permanent, cache.NoExpiration)
 	}
 
-	if err = machine.Run(ctx, m.socketPath(vm.Name), m.logsPath(vm.Name)); err != nil {
-		return m.withLogs(m.logsPath(vm.Name), err)
-	}
-
-	if err := m.waitAndAdjOom(ctx, vm.Name); err != nil {
-		return m.withLogs(m.logsPath(vm.Name), err)
-	}
-
-	return nil
-}
-
-func (m *Module) waitAndAdjOom(ctx context.Context, name string) error {
-	check := func() error {
-		if !m.Exists(name) {
-			return fmt.Errorf("failed to spawn vm machine process '%s'", name)
-		}
-		//TODO: check unix connection
-		socket := m.socketPath(name)
-		con, err := net.Dial("unix", socket)
-		if err != nil {
-			return err
-		}
-
-		con.Close()
-		return nil
-	}
-
-	ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
-	defer cancel()
-
-	if err := backoff.RetryNotify(
-		check,
-		backoff.WithContext(
-			backoff.NewConstantBackOff(2*time.Second),
-			ctx,
-		),
-		func(err error, d time.Duration) {
-			log.Debug().Err(err).Str("id", name).Msg("vm is not up yet")
-		}); err != nil {
-
-		return err
-	}
-
-	ps, err := Find(name)
+	machineInfo, err := machine.Run(ctx, m.socketPath(vm.Name), m.logsPath(vm.Name))
 	if err != nil {
-		return errors.Wrapf(err, "failed to find vm with id '%s'", name)
+		return pkg.MachineInfo{}, m.withLogs(m.logsPath(vm.Name), err)
 	}
 
-	if err := os.WriteFile(filepath.Join("/proc/", fmt.Sprint(ps.Pid), "oom_adj"), []byte("-17"), 0644); err != nil {
-		return errors.Wrapf(err, "failed to update oom priority for machine '%s' (PID: %d)", name, ps.Pid)
-	}
-
-	return nil
+	return machineInfo, nil
 }
 
 // Logs returns machine logs for give machine name
@@ -531,14 +502,14 @@ func (m *Module) Inspect(name string) (pkg.VMInfo, error) {
 		return pkg.VMInfo{}, fmt.Errorf("machine '%s' does not exist", name)
 	}
 	client := NewClient(m.socketPath(name))
-	cpu, mem, err := client.Inspect(context.Background())
+	vmdata, err := client.Inspect(context.Background())
 	if err != nil {
 		return pkg.VMInfo{}, errors.Wrap(err, "failed to get machine configuration")
 	}
 
 	return pkg.VMInfo{
-		CPU:       int64(cpu),
-		Memory:    int64(mem),
+		CPU:       int64(vmdata.CPU),
+		Memory:    int64(vmdata.Memory),
 		HtEnabled: false,
 	}, nil
 }
diff --git a/pkg/vm/monitor.go b/pkg/vm/monitor.go
index 05c35cf3..8f72d35c 100644
--- a/pkg/vm/monitor.go
+++ b/pkg/vm/monitor.go
@@ -165,13 +165,8 @@ func (m *Module) monitorID(ctx context.Context, running map[string]Process, id s
 		}
 
 		log.Debug().Str("name", id).Msg("trying to restart the vm")
-		reason = vm.Run(ctx, m.socketPath(id), m.logsPath(id))
-		if reason == nil {
-			reason = m.waitAndAdjOom(ctx, id)
-		}
-
-		if reason != nil {
-			reason = m.withLogs(m.logsPath(id), reason)
+		if _, err = vm.Run(ctx, m.socketPath(id), m.logsPath(id)); err != nil {
+			reason = m.withLogs(m.logsPath(id), err)
 		}
 	} else {
 		reason = fmt.Errorf("deleting vm due to so many crashes")
diff --git a/qemu/overlay/bin/powerd b/qemu/overlay/bin/powerd
new file mode 120000
index 00000000..25eae5c7
--- /dev/null
+++ b/qemu/overlay/bin/powerd
@@ -0,0 +1,1 @@
+../../../bin/zos
\ No newline at end of file
diff --git a/qemu/overlay/etc/zinit/powerd.yaml b/qemu/overlay/etc/zinit/powerd.yaml
new file mode 120000
index 00000000..0057d88e
--- /dev/null
+++ b/qemu/overlay/etc/zinit/powerd.yaml
@@ -0,0 +1,1 @@
+../../../../etc/zinit/powerd.yaml
\ No newline at end of file


Test patch:
diff --git a/pkg/gridtypes/zos/gw_fqdn_test.go b/pkg/gridtypes/zos/gw_test.go
similarity index 73%
rename from pkg/gridtypes/zos/gw_fqdn_test.go
rename to pkg/gridtypes/zos/gw_test.go
index 1a57220a..13b215c3 100644
--- a/pkg/gridtypes/zos/gw_fqdn_test.go
+++ b/pkg/gridtypes/zos/gw_test.go
@@ -68,6 +68,52 @@ func TestValidBackend(t *testing.T) {
 	})
 }
 
+func TestAsAddress(t *testing.T) {
+
+	cases := []struct {
+		Backend Backend
+		Address string
+		Err     bool
+	}{
+		{
+			Backend: Backend("http://10.20.30.40"),
+			Address: "10.20.30.40:80",
+		},
+		{
+			Backend: Backend("[2001:db8:3333:4444:CCCC:DDDD:EEEE:FFFF]:200"),
+			Address: "[2001:db8:3333:4444:CCCC:DDDD:EEEE:FFFF]:200",
+		},
+		{
+			Backend: Backend("2001:db8:3333:4444:CCCC:DDDD:EEEE:FFFF:200"),
+			Err:     true,
+		},
+		{
+			Backend: Backend("http://[2001:db8:3333:4444:CCCC:DDDD:EEEE:FFFF]:200"),
+			Address: "[2001:db8:3333:4444:CCCC:DDDD:EEEE:FFFF]:200",
+		},
+		{
+			Backend: Backend("http://10.20.30.40:500"),
+			Address: "10.20.30.40:500",
+		},
+		{
+			Backend: Backend("10.20.30.40:500"),
+			Address: "10.20.30.40:500",
+		},
+	}
+
+	for _, c := range cases {
+		t.Run(string(c.Backend), func(t *testing.T) {
+			addr, err := c.Backend.AsAddress()
+			if c.Err {
+				require.Error(t, err)
+			} else {
+				require.NoError(t, err)
+				require.Equal(t, c.Address, addr)
+			}
+		})
+	}
+}
+
 func TestValidBackendIP6(t *testing.T) {
 	require := require.New(t)