Repository: scionproto/scion
Pull request #3739 (2020-04-29 09:54:44)

staticinfo: add config format, and non-generated wire structs.
This commit adds the static info configuration parsing and format implementation.
It also has code to convert the configuration format into the wire format.

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/scionproto/scion/3739)
<!-- Reviewable:end -->



Patch:
diff --git a/.gitignore b/.gitignore
index 5fef80172..b25f0922c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -79,3 +79,7 @@ tags
 
 # documentation builds
 _build
+
+# IntelliJ folder
+#########################
+.idea/
diff --git a/go/cs/beaconing/BUILD.bazel b/go/cs/beaconing/BUILD.bazel
index 491e4fe7b..9b110e36c 100644
--- a/go/cs/beaconing/BUILD.bazel
+++ b/go/cs/beaconing/BUILD.bazel
@@ -10,6 +10,7 @@ go_library(
         "originator.go",
         "propagator.go",
         "registrar.go",
+        "staticinfo_config.go",
         "tick.go",
         "util.go",
     ],
@@ -49,6 +50,7 @@ go_test(
         "originator_test.go",
         "propagator_test.go",
         "registrar_test.go",
+        "staticinfo_config_test.go",
     ],
     data = glob(["testdata/**"]),
     embed = [":go_default_library"],
diff --git a/go/cs/beaconing/extender.go b/go/cs/beaconing/extender.go
index 2f7bb89f3..17df7e6d5 100644
--- a/go/cs/beaconing/extender.go
+++ b/go/cs/beaconing/extender.go
@@ -25,6 +25,7 @@ import (
 	"github.com/scionproto/scion/go/lib/log"
 	"github.com/scionproto/scion/go/lib/serrors"
 	"github.com/scionproto/scion/go/lib/spath"
+	"github.com/scionproto/scion/go/lib/topology"
 	"github.com/scionproto/scion/go/lib/util"
 )
 
@@ -65,6 +66,8 @@ func (s *segExtender) extend(pseg *seg.PathSegment, inIfid, egIfid common.IFIDTy
 	if err != nil {
 		return err
 	}
+	staticInfoPeers := createPeerMap(s.cfg)
+	staticInfo := GenerateStaticinfo(s.cfg.StaticInfoCfg, staticInfoPeers, egIfid, inIfid)
 	meta := s.cfg.Signer.Meta()
 	asEntry := &seg.ASEntry{
 		RawIA:      meta.Src.IA.IAInt(),
@@ -74,6 +77,7 @@ func (s *segExtender) extend(pseg *seg.PathSegment, inIfid, egIfid common.IFIDTy
 		MTU:        s.cfg.MTU,
 		HopEntries: hopEntries,
 	}
+	asEntry.Exts.StaticInfo = &staticInfo
 	if err := pseg.AddASEntry(asEntry, s.cfg.Signer); err != nil {
 		return err
 	}
@@ -197,3 +201,15 @@ func min(a, b spath.ExpTimeType) spath.ExpTimeType {
 	}
 	return a
 }
+
+// createPeerMap creates a set of peers indicating whether the
+// interface identified by the key is used for peering or not.
+func createPeerMap(cfg ExtenderConf) map[common.IFIDType]struct{} {
+	peers := make(map[common.IFIDType]struct{})
+	for ifID, ifInfo := range cfg.Intfs.All() {
+		if ifInfo.TopoInfo().LinkType == topology.Peer {
+			peers[ifID] = struct{}{}
+		}
+	}
+	return peers
+}
diff --git a/go/cs/beaconing/extender_config.go b/go/cs/beaconing/extender_config.go
index d7c5d9168..3a6ec464a 100644
--- a/go/cs/beaconing/extender_config.go
+++ b/go/cs/beaconing/extender_config.go
@@ -44,6 +44,8 @@ type ExtenderConf struct {
 	GetMaxExpTime func() spath.ExpTimeType
 	// task contains an identifier specific to the task that uses the extender.
 	task string
+	// StaticInfoCfg contains the Configdata used for the StaticInfo Extension.
+	StaticInfoCfg StaticInfoCfg
 }
 
 // InitDefaults initializes the default values, if not set.
diff --git a/go/cs/beaconing/staticinfo_config.go b/go/cs/beaconing/staticinfo_config.go
new file mode 100644
index 000000000..503c19457
--- /dev/null
+++ b/go/cs/beaconing/staticinfo_config.go
@@ -0,0 +1,218 @@
+package beaconing
+
+import (
+	"encoding/json"
+	"io/ioutil"
+	"math"
+
+	"github.com/scionproto/scion/go/lib/common"
+	"github.com/scionproto/scion/go/lib/ctrl/seg"
+	"github.com/scionproto/scion/go/lib/serrors"
+)
+
+type InterfaceLatencies struct {
+	Inter uint16                     `json:"Inter"`
+	Intra map[common.IFIDType]uint16 `json:"Intra"`
+}
+
+type InterfaceBandwidths struct {
+	Inter uint32                     `json:"Inter"`
+	Intra map[common.IFIDType]uint32 `json:"Intra"`
+}
+
+type InterfaceGeodata struct {
+	Longitude float32 `json:"Longitude"`
+	Latitude  float32 `json:"Latitude"`
+	Address   string  `json:"Address"`
+}
+
+type InterfaceHops struct {
+	Intra map[common.IFIDType]uint8 `json:"Intra"`
+}
+
+// StaticInfoCfg is used to parse data from config.json.
+type StaticInfoCfg struct {
+	Latency   map[common.IFIDType]InterfaceLatencies  `json:"Latency"`
+	Bandwidth map[common.IFIDType]InterfaceBandwidths `json:"Bandwidth"`
+	Linktype  map[common.IFIDType]string              `json:"Linktype"`
+	Geo       map[common.IFIDType]InterfaceGeodata    `json:"Geo"`
+	Hops      map[common.IFIDType]InterfaceHops       `json:"Hops"`
+	Note      string                                  `json:"Note"`
+}
+
+// gatherLatency extracts latency values from a StaticInfoCfg struct and
+// inserts them into the LatencyInfo portion of a StaticInfoExtn struct.
+func (cfgdata StaticInfoCfg) gatherLatency(peers map[common.IFIDType]struct{},
+	egifID common.IFIDType, inifID common.IFIDType) seg.LatencyInfo {
+
+	l := seg.LatencyInfo{
+		Egresslatency:          cfgdata.Latency[egifID].Inter,
+		IngressToEgressLatency: cfgdata.Latency[egifID].Intra[inifID],
+	}
+	for subintfid, intfdelay := range cfgdata.Latency[egifID].Intra {
+		// If we're looking at a peering interface, always include the data
+		if _, peer := peers[subintfid]; peer {
+			l.Peerlatencies = append(l.Peerlatencies, seg.PeerLatency{
+				IfID:       subintfid,
+				Interdelay: cfgdata.Latency[subintfid].Inter,
+				IntraDelay: intfdelay,
+			})
+			continue
+		}
+		// If we're looking at a NON-peering interface, only include the data
+		// if subintfid>egifID so as to not store redundant information
+		if subintfid > egifID {
+			l.Childlatencies = append(l.Childlatencies, seg.ChildLatency{
+				Intradelay: intfdelay,
+				IfID:       subintfid,
+			})
+		}
+	}
+	return l
+}
+
+// gatherBW extracts bandwidth values from a StaticInfoCfg struct and
+// inserts them into the BandwidthInfo portion of a StaticInfoExtn struct.
+func (cfgdata StaticInfoCfg) gatherBW(peers map[common.IFIDType]struct{}, egifID common.IFIDType,
+	inifID common.IFIDType) seg.BandwidthInfo {
+
+	l := seg.BandwidthInfo{
+		EgressBW:          cfgdata.Bandwidth[egifID].Inter,
+		IngressToEgressBW: cfgdata.Bandwidth[egifID].Intra[inifID],
+	}
+	for subintfid, intfbw := range cfgdata.Bandwidth[egifID].Intra {
+		// If we're looking at a peering interface, always include the data
+		if _, peer := peers[subintfid]; peer {
+			l.Bandwidths = append(l.Bandwidths, seg.InterfaceBandwidth{
+				IfID: subintfid,
+				BW: uint32(math.Min(float64(intfbw),
+					float64(cfgdata.Bandwidth[subintfid].Inter))),
+			})
+			continue
+		}
+		// If we're looking at a NON-peering interface, only include the
+		// data if subintfid>egifID so as to not store redundant information
+		if subintfid > egifID {
+			l.Bandwidths = append(l.Bandwidths, seg.InterfaceBandwidth{
+				BW:   intfbw,
+				IfID: subintfid,
+			})
+		}
+	}
+	return l
+}
+
+// transformLinkType transforms the linktype from a string into a
+// value that can be automatically parsed into a capnp enum.
+func transformLinkType(linktype string) uint16 {
+	switch linktype {
+	case "direct":
+		return 0
+	case "multihop":
+		return 1
+	case "opennet":
+		return 2
+	default:
+		return 2
+	}
+}
+
+// gatherLinktype extracts linktype values from a StaticInfoCfg struct and
+// inserts them into the LinktypeInfo portion of a StaticInfoExtn struct.
+func (cfgdata StaticInfoCfg) gatherLinkType(peers map[common.IFIDType]struct{},
+	egifID common.IFIDType) seg.LinktypeInfo {
+
+	l := seg.LinktypeInfo{
+		EgressLinkType: transformLinkType(cfgdata.Linktype[egifID]),
+	}
+	for intfid, intfLT := range cfgdata.Linktype {
+		// If we're looking at a peering interface, include the data for
+		// the peering link, otherwise drop it
+		if _, peer := peers[intfid]; peer {
+			l.Peerlinks = append(l.Peerlinks, seg.InterfaceLinkType{
+				LinkType: transformLinkType(intfLT),
+				IfID:     intfid,
+			})
+		}
+	}
+	return l
+}
+
+// gatherHops extracts hop values from a StaticInfoCfg struct and
+// inserts them into the InternalHopsinfo portion of a StaticInfoExtn struct.
+func (cfgdata StaticInfoCfg) gatherHops(peers map[common.IFIDType]struct{},
+	egifID common.IFIDType, inifID common.IFIDType) seg.InternalHopsInfo {
+
+	l := seg.InternalHopsInfo{
+		InToOutHops: cfgdata.Hops[egifID].Intra[inifID],
+	}
+	for intfid, intfHops := range cfgdata.Hops[egifID].Intra {
+		// If we're looking at a peering interface or intfid>egifID, include
+		// the data, otherwise drop it so as to not store redundant information
+		if _, peer := peers[intfid]; peer || (intfid > egifID) {
+			l.InterfaceHops = append(l.InterfaceHops, seg.InterfaceHops{
+				Hops: intfHops,
+				IfID: intfid,
+			})
+		}
+	}
+	return l
+}
+
+// gatherGeo extracts geo values from a StaticInfoCfg struct and
+// inserts them into the GeoInfo portion of a StaticInfoExtn struct.
+func (cfgdata StaticInfoCfg) gatherGeo() seg.GeoInfo {
+	l := seg.GeoInfo{}
+	for intfid, loc := range cfgdata.Geo {
+		assigned := false
+		for k := 0; k < len(l.Locations); k++ {
+			if (loc.Longitude == l.Locations[k].GPSData.Longitude) &&
+				(loc.Latitude == l.Locations[k].GPSData.Latitude) &&
+				(loc.Address == l.Locations[k].GPSData.Address) && (!assigned) {
+				l.Locations[k].IfIDs = append(l.Locations[k].IfIDs, intfid)
+				assigned = true
+			}
+		}
+		if !assigned {
+			l.Locations = append(l.Locations, seg.Location{
+				GPSData: seg.Coordinates{
+					Longitude: loc.Longitude,
+					Latitude:  loc.Latitude,
+					Address:   loc.Address,
+				},
+				IfIDs: []common.IFIDType{intfid},
+			})
+		}
+	}
+	return l
+}
+
+// ParseStaticInfoCfg parses data from a config file into a StaticInfoCfg struct.
+func ParseStaticInfoCfg(file string) (StaticInfoCfg, error) {
+	raw, err := ioutil.ReadFile(file)
+	if err != nil {
+		return StaticInfoCfg{}, serrors.WrapStr("failed to read static info config: ",
+			err, "file", file)
+	}
+	var cfg StaticInfoCfg
+	if err := json.Unmarshal(raw, &cfg); err != nil {
+		return StaticInfoCfg{}, serrors.WrapStr("failed to parse static info config: ",
+			err, "file ", file)
+	}
+	return cfg, nil
+}
+
+// GenerateStaticinfo creates a StaticinfoExtn struct and
+// populates it with data extracted from configdata.
+func GenerateStaticinfo(configdata StaticInfoCfg, peers map[common.IFIDType]struct{},
+	egifID common.IFIDType, inifID common.IFIDType) seg.StaticInfoExtn {
+
+	return seg.StaticInfoExtn{
+		Latency:   configdata.gatherLatency(peers, egifID, inifID),
+		Bandwidth: configdata.gatherBW(peers, egifID, inifID),
+		Linktype:  configdata.gatherLinkType(peers, egifID),
+		Geo:       configdata.gatherGeo(),
+		Note:      configdata.Note,
+		Hops:      configdata.gatherHops(peers, egifID, inifID),
+	}
+}
diff --git a/go/cs/main.go b/go/cs/main.go
index 678eb5eda..0f16bef18 100644
--- a/go/cs/main.go
+++ b/go/cs/main.go
@@ -79,6 +79,8 @@ import (
 var (
 	cfg config.Config
 
+	staticInfoCfg beaconing.StaticInfoCfg
+
 	intfs *ifstate.Interfaces
 	tasks *periodicTasks
 
@@ -583,6 +585,7 @@ func (t *periodicTasks) startPropagator(a *net.UDPAddr) (*periodic.Runner, error
 			MTU:           topo.MTU(),
 			Signer:        signer,
 			GetMaxExpTime: maxExpTimeFactory(t.store, beacon.PropPolicy),
+			StaticInfoCfg: staticInfoCfg,
 		},
 		Period: cfg.BS.PropagationInterval.Duration,
 	}.New()
@@ -727,6 +730,10 @@ func setup() error {
 	if err != nil {
 		return common.NewBasicError("Unable to load topology", err)
 	}
+	staticInfoCfg, err = beaconing.ParseStaticInfoCfg(cfg.General.StaticInfoConfig())
+	if err != nil {
+		log.Warn("Failed to read static info", "err", err)
+	}
 	// Use CS for monolith for now
 	itopo.Init(
 		&itopo.Config{
diff --git a/go/lib/ctrl/seg/BUILD.bazel b/go/lib/ctrl/seg/BUILD.bazel
index 8fd550761..d55112625 100644
--- a/go/lib/ctrl/seg/BUILD.bazel
+++ b/go/lib/ctrl/seg/BUILD.bazel
@@ -10,6 +10,7 @@ go_library(
         "seg.go",
         "segs.go",
         "signed.go",
+        "staticinfo_extn.go",
     ],
     importpath = "github.com/scionproto/scion/go/lib/ctrl/seg",
     visibility = ["//visibility:public"],
diff --git a/go/lib/ctrl/seg/as.go b/go/lib/ctrl/seg/as.go
index ebcd5db86..e3ca0297e 100644
--- a/go/lib/ctrl/seg/as.go
+++ b/go/lib/ctrl/seg/as.go
@@ -38,6 +38,7 @@ type ASEntry struct {
 		RoutingPolicy common.RawBytes    `capnp:"-"` // Not supported yet
 		Sibra         common.RawBytes    `capnp:"-"` // Not supported yet
 		HiddenPathSeg *HiddenPathSegExtn `capnp:"hiddenPathSeg"`
+		StaticInfo    *StaticInfoExtn    `capnp:"staticInfo"`
 	}
 }
 
diff --git a/go/lib/ctrl/seg/staticinfo_extn.go b/go/lib/ctrl/seg/staticinfo_extn.go
new file mode 100644
index 000000000..db1e68a7c
--- /dev/null
+++ b/go/lib/ctrl/seg/staticinfo_extn.go
@@ -0,0 +1,76 @@
+package seg
+
+import "github.com/scionproto/scion/go/lib/common"
+
+type LatencyInfo struct {
+	Egresslatency          uint16         `capnp:"egressLatency"`
+	IngressToEgressLatency uint16         `capnp:"ingressToEgressLatency"`
+	Childlatencies         []ChildLatency `capnp:"childLatencies"`
+	Peerlatencies          []PeerLatency  `capnp:"peeringLatencies"`
+}
+
+type ChildLatency struct {
+	Intradelay uint16          `capnp:"intra"`
+	IfID       common.IFIDType `capnp:"ifID"`
+}
+
+type PeerLatency struct {
+	Interdelay uint16          `capnp:"inter"`
+	IntraDelay uint16          `capnp:"intra"`
+	IfID       common.IFIDType `capnp:"ifID"`
+}
+
+type BandwidthInfo struct {
+	EgressBW          uint32               `capnp:"egressBW"`
+	IngressToEgressBW uint32               `capnp:"ingressToEgressBW"`
+	Bandwidths        []InterfaceBandwidth `capnp:"bandwidths"`
+}
+
+type InterfaceBandwidth struct {
+	BW   uint32          `capnp:"bw"`
+	IfID common.IFIDType `capnp:"ifID"`
+}
+
+type GeoInfo struct {
+	Locations []Location `capnp:"locations"`
+}
+
+type Location struct {
+	GPSData Coordinates       `capnp:"gpsData"`
+	IfIDs   []common.IFIDType `capnp:"interfaces"`
+}
+
+type Coordinates struct {
+	Latitude  float32 `capnp:"latitude"`
+	Longitude float32 `capnp:"longitude"`
+	Address   string  `capnp:"address"`
+}
+
+type LinktypeInfo struct {
+	EgressLinkType uint16              `capnp:"egressLinkType"`
+	Peerlinks      []InterfaceLinkType `capnp:"peeringLinks"`
+}
+
+type InterfaceLinkType struct {
+	IfID     common.IFIDType `capnp:"ifID"`
+	LinkType uint16          `capnp:"linkType"`
+}
+
+type InternalHopsInfo struct {
+	InToOutHops   uint8           `capnp:"inToOutHops"`
+	InterfaceHops []InterfaceHops `capnp:"interfaceHops"`
+}
+
+type InterfaceHops struct {
+	Hops uint8           `capnp:"hops"`
+	IfID common.IFIDType `capnp:"ifID"`
+}
+
+type StaticInfoExtn struct {
+	Latency   LatencyInfo      `capnp:"latency"`
+	Geo       GeoInfo          `capnp:"geo"`
+	Linktype  LinktypeInfo     `capnp:"linktype"`
+	Bandwidth BandwidthInfo    `capnp:"bandwidth"`
+	Hops      InternalHopsInfo `capnp:"internalHops"`
+	Note      string           `capnp:"note"`
+}
diff --git a/go/lib/env/env.go b/go/lib/env/env.go
index d9bff991a..a845c61e8 100644
--- a/go/lib/env/env.go
+++ b/go/lib/env/env.go
@@ -49,6 +49,10 @@ const (
 	// TopologyFile is the file name for the topology file.
 	TopologyFile = "topology.json"
 
+	// StaticinfoConfigFile is the file name for the configuration file
+	// used for the StaticInfo beacon extension.
+	StaticInfoConfigFile = "staticInfoConfig.json"
+
 	// SciondInitConnectPeriod is the default total amount of time spent
 	// attempting to connect to sciond on start.
 	SciondInitConnectPeriod = 20 * time.Second
@@ -72,7 +76,7 @@ type General struct {
 	// ID is the SCION element ID. This is used to choose the relevant
 	// portion of the topology file for some services.
 	ID string `toml:"id,omitempty"`
-	// ConfigDir for loading extra files (currently, only topology.json)
+	// ConfigDir for loading extra files (currently, only topology.json and staticInfoConfig.json)
 	ConfigDir string `toml:"config_dir,omitempty"`
 	// ReconnectToDispatcher can be set to true to enable transparent dispatcher
 	// reconnects.
@@ -117,6 +121,11 @@ func (cfg *General) Topology() string {
 	return filepath.Join(cfg.ConfigDir, TopologyFile)
 }
 
+// StaticInfoConfig return the path to the configuration file for the StaticInfo beacon extension.
+func (cfg *General) StaticInfoConfig() string {
+	return filepath.Join(cfg.ConfigDir, StaticInfoConfigFile)
+}
+
 var _ config.Config = (*SCIONDClient)(nil)
 
 // SCIONDClient contains information for running snet with sciond.


Test patch:
diff --git a/go/cs/beaconing/staticinfo_config_test.go b/go/cs/beaconing/staticinfo_config_test.go
new file mode 100644
index 000000000..e0beb511e
--- /dev/null
+++ b/go/cs/beaconing/staticinfo_config_test.go
@@ -0,0 +1,237 @@
+package beaconing
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/scionproto/scion/go/lib/common"
+	"github.com/scionproto/scion/go/lib/ctrl/seg"
+)
+
+func getTestConfigData() StaticInfoCfg {
+	return StaticInfoCfg{
+		Latency: map[common.IFIDType]InterfaceLatencies{
+			1: {
+				Inter: 30,
+				Intra: map[common.IFIDType]uint16{2: 10, 3: 20, 5: 30},
+			},
+			2: {
+				Inter: 40,
+				Intra: map[common.IFIDType]uint16{1: 10, 3: 70, 5: 50},
+			},
+			3: {
+				Inter: 80,
+				Intra: map[common.IFIDType]uint16{1: 20, 2: 70, 5: 60},
+			},
+			5: {
+				Inter: 90,
+				Intra: map[common.IFIDType]uint16{1: 30, 2: 50, 3: 60},
+			},
+		},
+		Bandwidth: map[common.IFIDType]InterfaceBandwidths{
+			1: {
+				Inter: 400000000,
+				Intra: map[common.IFIDType]uint32{2: 100000000, 3: 200000000, 5: 300000000},
+			},
+			2: {
+				Inter: 5000000,
+				Intra: map[common.IFIDType]uint32{1: 5044444, 3: 6555550, 5: 75555550},
+			},
+			3: {
+				Inter: 80,
+				Intra: map[common.IFIDType]uint32{1: 9333330, 2: 1044440, 5: 1333310},
+			},
+			5: {
+				Inter: 120,
+				Intra: map[common.IFIDType]uint32{1: 1333330, 2: 1555540, 3: 15666660},
+			},
+		},
+		Linktype: map[common.IFIDType]string{1: "direct", 2: "opennet", 3: "multihop", 5: "direct"},
+		Geo: map[common.IFIDType]InterfaceGeodata{
+			1: {
+				Longitude: 62.2,
+				Latitude:  47.2,
+				Address:   "geo1",
+			},
+			2: {
+				Longitude: 45.2,
+				Latitude:  79.2,
+				Address:   "geo2",
+			},
+			3: {
+				Longitude: 42.23,
+				Latitude:  47.22,
+				Address:   "geo3",
+			},
+			5: {
+				Longitude: 46.2,
+				Latitude:  48.2,
+				Address:   "geo5",
+			},
+		},
+		Hops: map[common.IFIDType]InterfaceHops{
+			1: {
+				Intra: map[common.IFIDType]uint8{2: 2, 3: 3, 5: 0},
+			},
+			2: {
+				Intra: map[common.IFIDType]uint8{1: 2, 3: 3, 5: 1},
+			},
+			3: {
+				Intra: map[common.IFIDType]uint8{1: 4, 2: 6, 5: 3},
+			},
+			5: {
+				Intra: map[common.IFIDType]uint8{1: 2, 2: 3, 3: 4},
+			},
+		},
+		Note: "asdf",
+	}
+}
+
+// TestParsing tests whether or not ParseStaticInfoCfg works properly.
+func TestParsing(t *testing.T) {
+	expected := getTestConfigData()
+	actual, err := ParseStaticInfoCfg("testdata/testconfigfile.json")
+	assert.NoError(t, err, "error occurred during parsing")
+	assert.Equal(t, expected, actual)
+}
+
+// TestGenerateStaticinfo tests whether or not GenerateStaticinfo works properly.
+func TestGenerateStaticinfo(t *testing.T) {
+	test := struct {
+		configData StaticInfoCfg
+		peers      map[common.IFIDType]struct{}
+		egIfid     common.IFIDType
+		inIfid     common.IFIDType
+		expected   seg.StaticInfoExtn
+	}{
+		configData: getTestConfigData(),
+		peers:      map[common.IFIDType]struct{}{5: {}},
+		egIfid:     2,
+		inIfid:     3,
+		expected: seg.StaticInfoExtn{
+			Latency: seg.LatencyInfo{
+				Egresslatency:          40,
+				IngressToEgressLatency: 70,
+				Childlatencies: []seg.ChildLatency{
+					{
+						Intradelay: 70,
+						IfID:       3,
+					},
+				},
+				Peerlatencies: []seg.PeerLatency{
+					{
+						Interdelay: 90,
+						IntraDelay: 50,
+						IfID:       5,
+					},
+				},
+			},
+			Geo: seg.GeoInfo{
+				Locations: []seg.Location{
+					{
+						GPSData: seg.Coordinates{
+							Latitude:  47.2,
+							Longitude: 62.2,
+							Address:   "geo1",
+						},
+						IfIDs: []common.IFIDType{1},
+					},
+					{
+						GPSData: seg.Coordinates{
+							Latitude:  79.2,
+							Longitude: 45.2,
+							Address:   "geo2",
+						},
+						IfIDs: []common.IFIDType{2},
+					},
+					{
+						GPSData: seg.Coordinates{
+							Latitude:  47.22,
+							Longitude: 42.23,
+							Address:   "geo3",
+						},
+						IfIDs: []common.IFIDType{3},
+					},
+					{
+						GPSData: seg.Coordinates{
+							Latitude:  48.2,
+							Longitude: 46.2,
+							Address:   "geo5",
+						},
+						IfIDs: []common.IFIDType{5},
+					},
+				},
+			},
+			Linktype: seg.LinktypeInfo{
+				EgressLinkType: 2,
+				Peerlinks: []seg.InterfaceLinkType{
+					{
+						IfID:     5,
+						LinkType: 0,
+					},
+				},
+			},
+			Bandwidth: seg.BandwidthInfo{
+				EgressBW:          5000000,
+				IngressToEgressBW: 6555550,
+				Bandwidths: []seg.InterfaceBandwidth{
+					{
+						IfID: 3,
+						BW:   6555550,
+					},
+					{
+						IfID: 5,
+						BW:   120,
+					},
+				},
+			},
+			Hops: seg.InternalHopsInfo{
+				InToOutHops: 3,
+				InterfaceHops: []seg.InterfaceHops{
+					{
+						IfID: 3,
+						Hops: 3,
+					},
+					{
+						IfID: 5,
+						Hops: 1,
+					},
+				},
+			},
+			Note: "asdf",
+		},
+	}
+	actual := GenerateStaticinfo(test.configData,
+		test.peers, test.egIfid, test.inIfid)
+	assert.Equal(t, test.expected.Latency.Egresslatency,
+		actual.Latency.Egresslatency)
+	assert.Equal(t, test.expected.Latency.IngressToEgressLatency,
+		actual.Latency.IngressToEgressLatency)
+	assert.ElementsMatch(t, test.expected.Latency.Childlatencies,
+		actual.Latency.Childlatencies)
+	assert.ElementsMatch(t, test.expected.Latency.Peerlatencies,
+		actual.Latency.Peerlatencies)
+
+	assert.Equal(t, test.expected.Bandwidth.IngressToEgressBW,
+		actual.Bandwidth.IngressToEgressBW)
+	assert.Equal(t, test.expected.Bandwidth.EgressBW,
+		actual.Bandwidth.EgressBW)
+	assert.ElementsMatch(t, test.expected.Bandwidth.Bandwidths,
+		actual.Bandwidth.Bandwidths)
+
+	assert.Equal(t, test.expected.Linktype.EgressLinkType,
+		actual.Linktype.EgressLinkType)
+	assert.ElementsMatch(t, test.expected.Linktype.Peerlinks,
+		actual.Linktype.Peerlinks)
+
+	assert.Equal(t, test.expected.Hops.InToOutHops,
+		actual.Hops.InToOutHops)
+	assert.ElementsMatch(t, test.expected.Hops.InterfaceHops,
+		actual.Hops.InterfaceHops)
+
+	assert.ElementsMatch(t, test.expected.Geo.Locations,
+		actual.Geo.Locations)
+
+	assert.Equal(t, test.expected.Note, actual.Note)
+}
diff --git a/go/cs/beaconing/testdata/testconfigfile.json b/go/cs/beaconing/testdata/testconfigfile.json
new file mode 100644
index 000000000..63f8ae4ae
--- /dev/null
+++ b/go/cs/beaconing/testdata/testconfigfile.json
@@ -0,0 +1,129 @@
+{
+  "Latency": {
+    "1":{
+      "Inter": 30,
+      "Intra": {
+        "2": 10,
+        "3": 20,
+        "5": 30
+      }
+    },
+    "2":{
+      "Inter": 40,
+      "Intra": {
+        "1": 10,
+        "3": 70,
+        "5": 50
+      }
+    },
+    "3":{
+      "Inter": 80,
+      "Intra": {
+        "1": 20,
+        "2": 70,
+        "5": 60
+      }
+    },
+    "5":{
+      "Inter": 90,
+      "Intra": {
+        "1": 30,
+        "2": 50,
+        "3": 60
+      }
+    }
+  },
+  "Bandwidth": {
+    "1":{
+      "Inter": 400000000,
+      "Intra": {
+        "2": 100000000,
+        "3": 200000000,
+        "5": 300000000
+      }
+    },
+    "2":{
+      "Inter": 5000000,
+      "Intra": {
+        "1": 5044444,
+        "3": 6555550,
+        "5": 75555550
+      }
+    },
+    "3":{
+      "Inter": 80,
+      "Intra": {
+        "1": 9333330,
+        "2": 1044440,
+        "5": 1333310
+      }
+    },
+    "5":{
+      "Inter": 120,
+      "Intra": {
+        "1": 1333330,
+        "2": 1555540,
+        "3": 15666660
+      }
+    }
+  },
+  "Linktype": {
+    "1":"direct",
+    "2":"opennet",
+    "3":"multihop",
+    "5":"direct"
+  },
+  "Geo": {
+    "1":{
+      "Latitude": 47.2,
+      "Longitude": 62.2,
+      "Address": "geo1"
+    },
+    "2":{
+      "Latitude": 79.2,
+      "Longitude": 45.2,
+      "Address": "geo2"
+    },
+    "3":{
+      "Latitude": 47.22,
+      "Longitude": 42.23,
+      "Address": "geo3"
+    },
+    "5":{
+      "Latitude": 48.2,
+      "Longitude": 46.2,
+      "Address": "geo5"
+    }
+  },
+  "Hops": {
+    "1":{
+      "Intra": {
+        "2": 2,
+        "3": 3,
+        "5": 0
+      }
+    },
+    "2":{
+      "Intra": {
+        "1": 2,
+        "3": 3,
+        "5": 1
+      }
+    },
+    "3":{
+      "Intra": {
+        "1": 4,
+        "2": 6,
+        "5": 3
+      }
+    },
+    "5":{
+      "Intra": {
+        "1": 2,
+        "2": 3,
+        "3": 4
+      }
+    }
+  },
+  "Note": "asdf"
+}