Commit aae5c7fc authored by Yann Garcia's avatar Yann Garcia
Browse files

Create Simu5G skeletons

parent e8b879f7
Loading
Loading
Loading
Loading
+16 −0
Original line number Diff line number Diff line
# meep-simu5g-bridge

Runs on **Edge-Native Connector**. Forwards IP traffic between UE/MEC pod TUN interfaces and the Simu5G bridge module on **Network Emulation Stack (Simu5G/SimuLTE)** over TCP.

## Two-host model

| Host | Role |
|------|------|
| *Edge-Native Connector | TUN + TCP **client**`deployment.simHost` |
| Network Emulation Stack | `Simu5gBridgeInterface` TCP **server** |

## Run

```bash
go run . --config /path/to/simu5g-config.json
```
+262 −0
Original line number Diff line number Diff line
/*
 * Copyright (c) 2026  The AdvantEDGE Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 */

/**
 * @file bridge.go
 * @brief TUN ↔ TCP bridge instance for sandbox host B.
 *
 * @details
 * Reads raw IPv4 packets from the TUN device, maps source IP to ueMappings index,
 * encodes DATA_UL frames, and sends them to simulator host S. Downlink DATA_DL
 * frames are written back to the TUN. TCP disconnects trigger exponential backoff reconnect.
 *
 * @ingroup simu5g_bridge_app
 */
package main

import (
	"context"
	"encoding/binary"
	"errors"
	"io"
	"net"
	"sync"
	"time"

	log "github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-logger"
	bridge "github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-simu5g-bridge"
)

/** @brief TCP dial timeout for initial and reconnect attempts. */
const (
	dialTimeout      = 10 * time.Second
	reconnectBackoff = 2 * time.Second
	maxReconnectWait = 30 * time.Second
)

/**
 * @brief Runtime state for one bridge process on host B.
 */
type BridgeInstance struct {
	cfg   *bridge.IntegrationConfig // Shared integration configuration
	tun   io.ReadWriteCloser       // TUN device (raw IP read/write)
	conn  net.Conn                 // TCP connection to Simu5gBridgeInterface on S
	mutex sync.Mutex               // Protects conn during reconnect and writes
}

/**
 * @brief Constructs a bridge instance: validates config, opens TUN, connects TCP, sends HELLO.
 * @param cfg Integration configuration (deployment, bridge, ueMappings).
 * @return Bridge instance or error from validation, TUN open, or TCP dial.
 */
func NewBridgeInstance(cfg *bridge.IntegrationConfig) (*BridgeInstance, error) {
	if err := cfg.ValidateForBridge(); err != nil {
		return nil, err
	}
	tunName := cfg.TunInterfaceName()
	tun, err := openTun(tunName)
	if err != nil {
		return nil, err
	}
	b := &BridgeInstance{cfg: cfg, tun: tun}
	log.Info("TUN opened: ", tunName, " (IFF_TUN|IFF_NO_PI, raw IP)")
	if err := b.connectAndHello(); err != nil {
		_ = tun.Close()
		return nil, err
	}
	log.Info("Bridge connected to simulator host S at ", cfg.BridgeTCPAddr(),
		" (sandbox host B, scenario=", cfg.Deployment.ScenarioID, ")")
	return b, nil
}

/**
 * @brief Dials simulator host S and sends a HELLO control frame.
 * @return Error from net.DialTimeout or TCP write.
 */
func (b *BridgeInstance) connectAndHello() error {
	addr := b.cfg.BridgeTCPAddr()
	conn, err := net.DialTimeout("tcp", addr, dialTimeout)
	if err != nil {
		return err
	}
	b.mutex.Lock()
	if b.conn != nil {
		_ = b.conn.Close()
	}
	b.conn = conn
	b.mutex.Unlock()

	hello := bridge.EncodeControlHello("bridge")
	b.mutex.Lock()
	_, err = b.conn.Write(hello)
	b.mutex.Unlock()
	return err
}

/**
 * @brief Runs uplink and downlink relay goroutines until context cancel or fatal error.
 * @param ctx Cancellation stops loops cleanly; TCP errors trigger reconnect.
 * @return ctx.Err() on shutdown, or error if reconnect is aborted.
 */
func (b *BridgeInstance) Run(ctx context.Context) error {
	defer func() {
		b.mutex.Lock()
		if b.conn != nil {
			_ = b.conn.Close()
		}
		b.mutex.Unlock()
	}()
	defer b.tun.Close()

	for {
		errCh := make(chan error, 2)
		go func() { errCh <- b.loopTunToSocket(ctx) }()
		go func() { errCh <- b.loopSocketToTun(ctx) }()

		select {
		case <-ctx.Done():
			return nil
		case err := <-errCh:
			if err == nil {
				continue
			}
			if ctx.Err() != nil {
				return nil
			}
			log.Warn("Bridge loop error: ", err, " — reconnecting to ", b.cfg.BridgeTCPAddr())
			if rerr := b.reconnect(ctx); rerr != nil {
				return rerr
			}
		}
	}
}

/**
 * @brief Reconnects to S with exponential backoff until success or context cancel.
 * @param ctx Used to abort the retry loop.
 * @return nil on successful reconnect, or ctx.Err().
 */
func (b *BridgeInstance) reconnect(ctx context.Context) error {
	wait := reconnectBackoff
	for {
		select {
		case <-ctx.Done():
			return ctx.Err()
		default:
		}
		if err := b.connectAndHello(); err == nil {
			log.Info("Reconnected to simulator at ", b.cfg.BridgeTCPAddr())
			return nil
		}
		time.Sleep(wait)
		if wait < maxReconnectWait {
			wait *= 2
		}
	}
}

/**
 * @brief Uplink loop: TUN read → resolve ueId → DATA_UL → TCP write.
 * @param ctx Exit loop when cancelled.
 * @return Read/write error (triggers reconnect in Run), or nil on ctx.Done().
 */
func (b *BridgeInstance) loopTunToSocket(ctx context.Context) error {
	buf := make([]byte, 65535)
	for {
		select {
		case <-ctx.Done():
			return nil
		default:
		}
		n, err := b.tun.Read(buf)
		if err != nil {
			return err
		}
		if n <= 0 {
			continue
		}
		pkt := buf[:n]
		ueID := uint16(0)
		if src := ipv4Source(pkt); src != nil {
			if idx := b.cfg.ResolveUeIndexFromIP(src); idx >= 0 {
				ueID = uint16(idx)
			}
		}
		frame := bridge.EncodeDataULFrame(ueID, pkt, 0)
		b.mutex.Lock()
		conn := b.conn
		_, err = conn.Write(frame)
		b.mutex.Unlock()
		if err != nil {
			return err
		}
	}
}

/**
 * @brief Downlink loop: TCP read → decode frame → DATA_DL IP packet → TUN write.
 * @param ctx Exit loop when cancelled.
 * @return Read/write error or invalid magic (triggers reconnect).
 */
func (b *BridgeInstance) loopSocketToTun(ctx context.Context) error {
	hdr := make([]byte, bridge.FrameHeaderSize)
	for {
		select {
		case <-ctx.Done():
			return nil
		default:
		}

		b.mutex.Lock()
		conn := b.conn
		b.mutex.Unlock()

		_, err := io.ReadFull(conn, hdr)
		if err != nil {
			return err
		}
		if binary.BigEndian.Uint16(hdr[0:2]) != bridge.FrameMagic {
			return errors.New("invalid frame magic")
		}
		length := binary.BigEndian.Uint32(hdr[4:8])
		payload := make([]byte, int(length))
		_, err = io.ReadFull(conn, payload)
		if err != nil {
			return err
		}
		switch hdr[3] {
		case bridge.FrameTypeDataDL:
			packet, ok := bridge.PacketFromDataDLFrame(payload)
			if !ok {
				continue
			}
			_, err = b.tun.Write(packet)
			if err != nil {
				return err
			}
			log.Debug("Delivered downlink packet bytes=", len(packet))
		case bridge.FrameTypeControl:
			log.Debug("Bridge control frame code=", payload[0])
		default:
			// ignore unknown types
		}
	}
}

/**
 * @brief Extracts IPv4 source address from a raw IPv4 packet buffer.
 * @param pkt Raw L3 packet bytes (minimum 20-byte IPv4 header).
 * @return Source IPv4 or nil if not IPv4 or buffer too short.
 */
func ipv4Source(pkt []byte) net.IP {
	if len(pkt) < 20 {
		return nil
	}
	if pkt[0]>>4 != 4 {
		return nil
	}
	return net.IP(pkt[12:16]).To4()
}
+18 −0
Original line number Diff line number Diff line
/*
 * Copyright (c) 2026  The AdvantEDGE Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 */

/**
 * @file doc.go
 * @brief Sandbox-side Simu5G user-plane bridge (host B).
 *
 * @details
 * Relays raw IP between TUN (simu5g-br0) and the TCP bridge on simulator host S.
 * See go-packages/meep-simu5g-bridge for shared protocol and configuration.
 *
 * @defgroup simu5g_bridge_app meep-simu5g-bridge (application)
 * @{
 */
package main
+13 −0
Original line number Diff line number Diff line
module github.com/InterDigitalInc/AdvantEDGE/go-apps/meep-simu5g-bridge

go 1.12

require (
	github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-logger v0.0.0
	github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-simu5g-bridge v0.0.0
)

replace (
	github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-logger => ../../go-packages/meep-logger
	github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-simu5g-bridge => ../../go-packages/meep-simu5g-bridge
)
+9 −0
Original line number Diff line number Diff line
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
Loading