Repository: scionproto/scion
Pull request #3550 (2019-12-20 09:24:36)

Migrate sig_failover_acceptance to bazel
Also:
- add support for plugging a configurable fake SCIOND into the SIG; this
makes it possible to run SIG tests without a control-plane
- change the entrypoint of docker debug images from su-exec; it is not needed
in debugging/development environments
- change UDP proxy command line options; the new proxy uses
deterministic ports when forwarding data
- add config option AllowRunAsRoot to allow running the SIG and Go
dispatcher as root
- use ping instead of using go/acceptance/sig_ping_acceptance for
connectivity tests
- use static docker-compose file instead of topology generator
to set up sig_failover_acceptance; the generator builds more
files than necessary for the test, and also does not have
support for fake sciond data; doing it directly from hand crafted
files is easier to inspect, modify and debug;
- remove BS, CS, PS, SD, BRs from sig_failover_acceptance

Fixes https://github.com/scionproto/scion/issues/3125.

<!-- 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/3550)
<!-- Reviewable:end -->



Patch:
diff --git a/.buildkite/hooks/pre-artifact b/.buildkite/hooks/pre-artifact
index cdc036769..b5ace4049 100755
--- a/.buildkite/hooks/pre-artifact
+++ b/.buildkite/hooks/pre-artifact
@@ -17,17 +17,34 @@ if [ "$BUILDKITE_PULL_REQUEST" == "false" ]; then
     TARGET="$BUILDKITE_BRANCH"
 fi
 TARGET="${TARGET//\//_}"
+echo "\$TARGET=$TARGET"
+
 # For nightly builds instead of the build number print nightly and the date.
 BUILD="build-${BUILDKITE_BUILD_NUMBER}"
 [ -n "$NIGHTLY" ] && BUILD=nightly-"$(date +%s)"
+echo "\$BUILD=$BUILD"
 
 ARTIFACTS="buildkite.${BUILDKITE_ORGANIZATION_SLUG}.${TARGET}.${BUILD}.${BUILDKITE_STEP_KEY}.${BUILDKITE_JOB_ID}"
+echo "\$ARTIFACTS=$ARTIFACTS"
+
 ARTIFACTS_DIR="artifacts"
+echo "\$ARTIFACTS_DIR=$ARTIFACTS_DIR"
+
+function save {
+	if [ -d "$1" ]; then
+		echo Found artifacts: "$1"
+		find "$1"
+		cp -R "$1" "$ARTIFACTS_DIR/$ARTIFACTS"
+	fi
+}
+
 mkdir -p "$ARTIFACTS_DIR/$ARTIFACTS" artifacts.out
-[ ! -d "bazel-testlogs" ] || cp -R "bazel-testlogs" "$ARTIFACTS_DIR/$ARTIFACTS"
-[ ! -d "logs" ] || cp -R "logs" "$ARTIFACTS_DIR/$ARTIFACTS"
-[ ! -d "traces" ] || cp -R "traces" "$ARTIFACTS_DIR/$ARTIFACTS"
-[ ! -d "gen" ] || cp -R "gen" "$ARTIFACTS_DIR/$ARTIFACTS"
-[ ! -d "gen-cache" ] || cp -R "gen-cache" "$ARTIFACTS_DIR/$ARTIFACTS"
-[ ! -d "accept_artifacts" ] || cp -R "accept_artifacts" "$ARTIFACTS_DIR/$ARTIFACTS"
+
+save "bazel-testlogs"
+save "logs"
+save "traces"
+save "gen"
+save "gen-cache"
+save "accept_artifacts"
+
 tar chaf "artifacts.out/$ARTIFACTS.tar.gz" -C "$ARTIFACTS_DIR" "$ARTIFACTS"
diff --git a/.buildkite/hooks/pre-command b/.buildkite/hooks/pre-command
index 72e406047..fb4b148b5 100755
--- a/.buildkite/hooks/pre-command
+++ b/.buildkite/hooks/pre-command
@@ -6,6 +6,8 @@ echo "Clean existing environment"
 
 . .buildkite/hooks/pre-exit
 
+export ACCEPTANCE_ARTIFACTS="$PWD/accept_artifacts"
+
 echo "Starting bazel remote cache proxy"
 
 # Start bazel remote cache proxy for S3
diff --git a/.buildkite/pipeline2.sh b/.buildkite/pipeline2.sh
index ab9beb6be..185e12c9f 100755
--- a/.buildkite/pipeline2.sh
+++ b/.buildkite/pipeline2.sh
@@ -13,7 +13,6 @@ gen_acceptance() {
         echo "      - ./acceptance/ctl grun $name"
         echo "    key: ${name}_acceptance"
         echo "    env:"
-        echo "      ACCEPTANCE_ARTIFACTS: \"\$BUILDKITE_BUILD_CHECKOUT_PATH/accept_artifacts\""
         echo "      PYTHONPATH: \"python/:.\""
         echo "    artifact_paths:"
         echo "      - \"artifacts.out/**/*\""
diff --git a/.buildkite/pipeline_buildlint.yml b/.buildkite/pipeline_buildlint.yml
index 65b1c0220..587f0c0b4 100644
--- a/.buildkite/pipeline_buildlint.yml
+++ b/.buildkite/pipeline_buildlint.yml
@@ -53,3 +53,14 @@ steps:
       automatic:
         - exit_status: -1  # Agent was lost
         - exit_status: 255 # Forced agent shutdown
+  - label: "Acceptance: sig_failover"
+    command:
+      - mkdir -p $$ACCEPTANCE_ARTIFACTS
+      - bazel test --action_env=ACCEPTANCE_ARTIFACTS //acceptance/sig_failover:sig_failover_test
+    key: sig_failover
+    retry:
+      automatic:
+        - exit_status: -1  # Agent was lost
+        - exit_status: 255 # Forced agent shutdown
+    artifact_paths:
+      - "artifacts.out/**/*"
diff --git a/acceptance/sig_failover/BUILD.bazel b/acceptance/sig_failover/BUILD.bazel
new file mode 100644
index 000000000..918fa6c3a
--- /dev/null
+++ b/acceptance/sig_failover/BUILD.bazel
@@ -0,0 +1,59 @@
+load("@io_bazel_rules_docker//container:container.bzl", "container_image")
+
+sh_test(
+    name = "sig_failover_test",
+    size = "small",
+    timeout = "moderate", # TODO(scrye): remove this
+    srcs = ["test"],
+    deps = [
+        "@bazel_tools//tools/bash/runfiles",
+    ],
+    data = [
+        "//go/tools/udpproxy:udpproxy",
+        ":dispatcher1",
+        ":dispatcher2",
+        ":sig1",
+        ":sig2",
+        "docker-compose.yml",
+    ],
+)
+
+container_image(
+    name = "dispatcher1",
+    base = "//docker/perapp:dispatcher_go_debug",
+    files = ["testdata/1-ff00_0_110/dispatcher/disp.toml"],
+    entrypoint = ["/app/godispatcher", "-config", "/disp.toml"],
+    stamp = True,
+)
+
+container_image(
+    name = "dispatcher2",
+    base = "//docker/perapp:dispatcher_go_debug",
+    files = ["testdata/1-ff00_0_111/dispatcher/disp.toml"],
+    entrypoint = ["/app/godispatcher", "-config", "/disp.toml"],
+    stamp = True,
+)
+
+container_image(
+    name = "sig1",
+    base = "//docker/perapp:sig_debug",
+    files = [
+        "testdata/1-ff00_0_110/sig/cfg.json",
+        "testdata/1-ff00_0_110/sig/sig.toml",
+        "testdata/1-ff00_0_110/sig/fake_sciond.json",
+    ],
+    entrypoint = ["/app/sig", "-config", "/sig.toml"],
+    stamp = True,
+)
+
+container_image(
+    name = "sig2",
+    base = "//docker/perapp:sig_debug",
+    files = [
+        "testdata/1-ff00_0_111/sig/cfg.json",
+        "testdata/1-ff00_0_111/sig/sig.toml",
+        "testdata/1-ff00_0_111/sig/fake_sciond.json",
+    ],
+    entrypoint = ["/app/sig", "-config", "/sig.toml"],
+    stamp = True,
+)
diff --git a/acceptance/sig_failover/docker-compose.yml b/acceptance/sig_failover/docker-compose.yml
new file mode 100644
index 000000000..2b624fd25
--- /dev/null
+++ b/acceptance/sig_failover/docker-compose.yml
@@ -0,0 +1,108 @@
+networks:
+  bridge1:
+    driver: bridge
+    driver_opts:
+      com.docker.network.bridge.name: bridge1
+    ipam:
+      config:
+      - subnet: 242.254.100.0/24
+  bridge2:
+    driver: bridge
+    driver_opts:
+      com.docker.network.bridge.name: bridge2
+    ipam:
+      config:
+      - subnet: 242.254.200.0/24
+services:
+  patha:
+    command: [
+      "-local_x", "242.254.100.3:50000",
+      "-remote_x", "242.254.100.2:30041",
+      "-local_y", "242.254.200.3:50000",
+      "-remote_y", "242.254.200.2:30041",
+      ]
+    container_name: patha
+    image: bazel/go/tools/udpproxy:udpproxy
+    networks:
+      bridge1:
+        ipv4_address: 242.254.100.3
+      bridge2:
+        ipv4_address: 242.254.200.3
+  pathb:
+    command: [
+      "-local_x", "242.254.100.4:50000",
+      "-remote_x", "242.254.100.2:30041",
+      "-local_y", "242.254.200.4:50000",
+      "-remote_y", "242.254.200.2:30041",
+      ]
+    container_name: pathb
+    image: bazel/go/tools/udpproxy:udpproxy
+    networks:
+      bridge1:
+        ipv4_address: 242.254.100.4
+      bridge2:
+        ipv4_address: 242.254.200.4
+  dispatcher1:
+    container_name: dispatcher1
+    image: bazel/acceptance/sig_failover:dispatcher1
+    networks:
+      bridge1:
+        ipv4_address: 242.254.100.2
+    volumes:
+    - vol_logs:/share/logs:rw
+    - vol_scion_disp_sig1-ff00_0_110:/run/shm/dispatcher:rw
+  dispatcher2:
+    container_name: dispatcher2
+    image: bazel/acceptance/sig_failover:dispatcher2
+    networks:
+      bridge2:
+        ipv4_address: 242.254.200.2
+    volumes:
+    - vol_logs:/share/logs:rw
+    - vol_scion_disp_sig1-ff00_0_111:/run/shm/dispatcher:rw
+  sig1:
+    cap_add:
+    - NET_ADMIN
+    container_name: sig1
+    depends_on:
+    - dispatcher1
+    image: bazel/acceptance/sig_failover:sig1
+    network_mode: service:dispatcher1
+    privileged: true
+    volumes:
+    - vol_scion_disp_sig1-ff00_0_110:/run/shm/dispatcher:rw
+    - /dev/net/tun:/dev/net/tun
+    - vol_logs:/share/logs:rw
+  sig2:
+    cap_add:
+    - NET_ADMIN
+    container_name: sig2
+    depends_on:
+    - dispatcher2
+    image: bazel/acceptance/sig_failover:sig2
+    network_mode: service:dispatcher2
+    privileged: true
+    volumes:
+    - vol_scion_disp_sig1-ff00_0_111:/run/shm/dispatcher:rw
+    - /dev/net/tun:/dev/net/tun
+    - vol_logs:/share/logs:rw
+  tester1:
+    container_name: tester1
+    image: alpine
+    network_mode: service:dispatcher1
+    privileged: true
+  tester2:
+    container_name: tester2
+    image: alpine
+    network_mode: service:dispatcher2
+    privileged: true
+  log_exporter:
+    container_name: log_exporter
+    image: alpine
+    volumes:
+    - vol_logs:/share/logs:ro
+version: '2.4'
+volumes:
+  vol_logs: null
+  vol_scion_disp_sig1-ff00_0_110: null
+  vol_scion_disp_sig1-ff00_0_111: null
diff --git a/docker/perapp/scion_app.bzl b/docker/perapp/scion_app.bzl
index c59305efb..820ae2804 100644
--- a/docker/perapp/scion_app.bzl
+++ b/docker/perapp/scion_app.bzl
@@ -93,7 +93,7 @@ def scion_app_images(name, binary, appdir, workdir, entrypoint):
         base = "//docker/perapp:app_base_debug",
         tars = [":" + name + "_docker_files"],
         workdir = workdir,
-        entrypoint = ["/sbin/su-exec"] + entrypoint,
+        entrypoint = entrypoint,
         stamp = True,
         visibility = ["//visibility:public"],
     )
diff --git a/go/godispatcher/main.go b/go/godispatcher/main.go
index 7cf86994a..56e8844b6 100644
--- a/go/godispatcher/main.go
+++ b/go/godispatcher/main.go
@@ -172,7 +172,7 @@ func checkPerms() error {
 	if err != nil {
 		return common.NewBasicError("Error retrieving user", err)
 	}
-	if u.Uid == "0" {
+	if u.Uid == "0" && !cfg.Features.AllowRunAsRoot {
 		return serrors.New("Running as root is not allowed for security reasons")
 	}
 	return nil
diff --git a/go/lib/env/env.go b/go/lib/env/env.go
index 09ca71395..9182b3527 100644
--- a/go/lib/env/env.go
+++ b/go/lib/env/env.go
@@ -132,6 +132,9 @@ type SciondClient struct {
 	// InitialConnectPeriod is the maximum amount of time spent attempting to
 	// connect to sciond on start.
 	InitialConnectPeriod util.DurWrap
+	// FakeData can be used to replace the local SCIOND with a fake data source.
+	// It must point to a fake SCIOND configuration file.
+	FakeData string
 }
 
 func (cfg *SciondClient) InitDefaults() {
diff --git a/go/lib/env/features.go b/go/lib/env/features.go
index 956991b71..154c4a0f7 100644
--- a/go/lib/env/features.go
+++ b/go/lib/env/features.go
@@ -25,6 +25,7 @@ var _ config.Config = (*Features)(nil)
 // Add feature flags to this structure as needed.
 // Feature flags are always boolean. Don't use any other types here!
 type Features struct {
+	AllowRunAsRoot bool
 }
 
 func (cfg *Features) InitDefaults() {
diff --git a/go/lib/sciond/fake/BUILD.bazel b/go/lib/sciond/fake/BUILD.bazel
new file mode 100644
index 000000000..e14900081
--- /dev/null
+++ b/go/lib/sciond/fake/BUILD.bazel
@@ -0,0 +1,33 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "go_default_library",
+    srcs = ["fake.go"],
+    importpath = "github.com/scionproto/scion/go/lib/sciond/fake",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//go/lib/addr:go_default_library",
+        "//go/lib/common:go_default_library",
+        "//go/lib/ctrl/path_mgmt:go_default_library",
+        "//go/lib/sciond:go_default_library",
+        "//go/lib/serrors:go_default_library",
+        "//go/lib/snet:go_default_library",
+        "//go/lib/spath:go_default_library",
+        "//go/proto:go_default_library",
+    ],
+)
+
+go_test(
+    name = "go_default_test",
+    srcs = ["fake_test.go"],
+    data = glob(["testdata/**"]),
+    embed = [":go_default_library"],
+    deps = [
+        "//go/lib/addr:go_default_library",
+        "//go/lib/sciond:go_default_library",
+        "//go/lib/snet:go_default_library",
+        "//go/lib/xtest:go_default_library",
+        "@com_github_stretchr_testify//assert:go_default_library",
+        "@com_github_stretchr_testify//require:go_default_library",
+    ],
+)
diff --git a/go/lib/sciond/fake/fake.go b/go/lib/sciond/fake/fake.go
new file mode 100644
index 000000000..0caee798e
--- /dev/null
+++ b/go/lib/sciond/fake/fake.go
@@ -0,0 +1,204 @@
+// Copyright 2019 Anapaya Systems
+
+package fake
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net"
+	"time"
+
+	"github.com/scionproto/scion/go/lib/addr"
+	"github.com/scionproto/scion/go/lib/common"
+	"github.com/scionproto/scion/go/lib/ctrl/path_mgmt"
+	"github.com/scionproto/scion/go/lib/sciond"
+	"github.com/scionproto/scion/go/lib/serrors"
+	"github.com/scionproto/scion/go/lib/snet"
+	"github.com/scionproto/scion/go/lib/spath"
+	"github.com/scionproto/scion/go/proto"
+)
+
+// New creates a new fake SCIOND implementation using the data in the script.
+func New(script *Script) sciond.Connector {
+	return &connector{
+		script:       script,
+		creationTime: time.Now(),
+	}
+}
+
+// NewFromFile creates a new fake SCIOND implementation using the JSON representation in the file.
+func NewFromFile(file string) (sciond.Connector, error) {
+	b, err := ioutil.ReadFile(file)
+	if err != nil {
+		return nil, serrors.WrapStr("unable to read script from file", err)
+	}
+	var script Script
+	if err := json.Unmarshal(b, &script); err != nil {
+		return nil, serrors.WrapStr("unable to unmarshal script from JSON", err)
+	}
+	return New(&script), nil
+}
+
+// Script describes the path entries a fake SCIOND should respond with.
+type Script struct {
+	Entries []*Entry `json:"entries"`
+}
+
+// Entry describes a path reply.
+type Entry struct {
+	// Seconds describes the number of seconds that should pass after a fake SCIOND has been created
+	// before serving paths from the entry. The last entry whose timestamp has passed is selected.
+	// (so, if the seconds timestamps are 0, 4, 6, the paths selected at 5 seconds from creation
+	// would be the ones associated with timestamp 4)
+	Seconds int `json:"seconds"`
+	// Paths contains the paths for a fake SCIOND reply.
+	Paths []*Path `json:"paths"`
+}
+
+type Path struct {
+	JSONFingerprint string   `json:"fingerprint"`
+	JSONNextHop     *UDPAddr `json:"next_hop,omitempty"`
+	JSONIA          addr.IA  `json:"ia"`
+}
+
+func (p Path) Fingerprint() snet.PathFingerprint {
+	return snet.PathFingerprint(p.JSONFingerprint)
+}
+
+func (p Path) OverlayNextHop() *net.UDPAddr {
+	return (*net.UDPAddr)(p.JSONNextHop)
+}
+
+func (p Path) Path() *spath.Path {
+	return DummyPath()
+}
+
+// DummyPath creates a path that is reversible.
+func DummyPath() *spath.Path {
+	return &spath.Path{
+		Raw:    make(common.RawBytes, spath.InfoFieldLength+spath.HopFieldLength),
+		HopOff: spath.InfoFieldLength,
+	}
+}
+
+func (p Path) Interfaces() []snet.PathInterface {
+	return []snet.PathInterface{}
+}
+
+func (p Path) Destination() addr.IA {
+	return p.JSONIA
+}
+
+func (p Path) MTU() uint16 {
+	return 1500
+}
+
+func (p Path) Expiry() time.Time {
+	return time.Now().Add(time.Hour * 24 * 7 * 365 * 2) // 2 years
+}
+
+func (p Path) Copy() snet.Path {
+	return &Path{
+		JSONFingerprint: p.JSONFingerprint,
+		JSONNextHop: &UDPAddr{
+			IP:   append(p.JSONNextHop.IP[:0:0], p.JSONNextHop.IP...),
+			Port: p.JSONNextHop.Port,
+			Zone: p.JSONNextHop.Zone,
+		},
+		JSONIA: p.JSONIA,
+	}
+}
+
+func (p Path) String() string {
+	return fmt.Sprintf("FakePath(IA: %v, NextHop: %v, Fingerprint: %v)",
+		p.JSONIA, p.JSONNextHop, p.JSONFingerprint)
+}
+
+// UDPAddr decorates net.UDPAddr with custom JSON marshaling logic.
+type UDPAddr net.UDPAddr
+
+func (u *UDPAddr) MarshalText() ([]byte, error) {
+	s := (*net.UDPAddr)(u).String()
+	return []byte(s), nil
+}
+
+func (u *UDPAddr) UnmarshalText(text []byte) error {
+	address, err := net.ResolveUDPAddr("udp", string(text))
+	if err != nil {
+		return err
+	}
+	*u = UDPAddr(*address)
+	if u.IP.To4() != nil {
+		u.IP = u.IP.To4()
+	}
+	return nil
+}
+
+type connector struct {
+	creationTime time.Time
+
+	script *Script
+}
+
+func (c connector) Paths(_ context.Context, _, _ addr.IA, max uint16,
+	_ sciond.PathReqFlags) ([]snet.Path, error) {
+
+	secondsElapsed := int(time.Since(c.creationTime).Seconds())
+	intMax := int(max)
+
+	var entry *Entry
+	for i := 0; i < len(c.script.Entries); i++ {
+		entry = c.script.Entries[i]
+		if secondsElapsed <= entry.Seconds {
+			break
+		}
+	}
+	if entry == nil {
+		return nil, serrors.New("path not found")
+	}
+
+	if intMax > len(entry.Paths) {
+		intMax = len(entry.Paths)
+	}
+	return adapter(entry.Paths[:intMax]), nil
+}
+
+func adapter(paths []*Path) []snet.Path {
+	var snetPaths []snet.Path
+	for _, path := range paths {
+		snetPaths = append(snetPaths, path)
+	}
+	return snetPaths
+}
+
+func (c connector) ASInfo(ctx context.Context, ia addr.IA) (*sciond.ASInfoReply, error) {
+	panic("not implemented")
+}
+
+func (c connector) IFInfo(ctx context.Context,
+	ifs []common.IFIDType) (map[common.IFIDType]*net.UDPAddr, error) {
+
+	panic("not implemented")
+}
+
+func (c connector) SVCInfo(ctx context.Context,
+	svcTypes []proto.ServiceType) (*sciond.ServiceInfoReply, error) {
+
+	panic("not implemented")
+}
+
+func (c connector) RevNotificationFromRaw(ctx context.Context, b []byte) (*sciond.RevReply, error) {
+	panic("not implemented")
+}
+
+func (c connector) RevNotification(ctx context.Context,
+	sRevInfo *path_mgmt.SignedRevInfo) (*sciond.RevReply, error) {
+
+	panic("not implemented")
+}
+
+func (c connector) Close(ctx context.Context) error {
+	return nil
+}
diff --git a/go/sig/egress/iface/sesspath.go b/go/sig/egress/iface/sesspath.go
index 4cc67f34e..cbdcd4522 100644
--- a/go/sig/egress/iface/sesspath.go
+++ b/go/sig/egress/iface/sesspath.go
@@ -55,5 +55,5 @@ func (sp *SessPath) Copy() *SessPath {
 }
 
 func (sp *SessPath) String() string {
-	return fmt.Sprintf("Key: %s %s", sp.key[:8], fmt.Sprintf("%s", sp.path))
+	return fmt.Sprintf("Key: %s %s", sp.key[:8], sp.path)
 }
diff --git a/go/sig/egress/session/sessmon.go b/go/sig/egress/session/sessmon.go
index 1d42ab26b..9659f53e5 100644
--- a/go/sig/egress/session/sessmon.go
+++ b/go/sig/egress/session/sessmon.go
@@ -134,7 +134,7 @@ func (sm *sessMonitor) updatePaths() {
 			"oldExpiration", expTime,
 			"newExpiration", currPath.Path().Expiry(),
 			"oldMTU", mtu,
-			"newMTU", currPath.Path().Expiry())
+			"newMTU", currPath.Path().MTU)
 		sm.updateSessSnap()
 	}
 }
diff --git a/go/sig/internal/sigcmn/BUILD.bazel b/go/sig/internal/sigcmn/BUILD.bazel
index ed37bb7ae..9712762b5 100644
--- a/go/sig/internal/sigcmn/BUILD.bazel
+++ b/go/sig/internal/sigcmn/BUILD.bazel
@@ -10,6 +10,8 @@ go_library(
         "//go/lib/common:go_default_library",
         "//go/lib/env:go_default_library",
         "//go/lib/log:go_default_library",
+        "//go/lib/sciond/fake:go_default_library",
+        "//go/lib/serrors:go_default_library",
         "//go/lib/snet:go_default_library",
         "//go/lib/sock/reliable:go_default_library",
         "//go/sig/internal/pathmgr:go_default_library",
diff --git a/go/sig/internal/sigcmn/common.go b/go/sig/internal/sigcmn/common.go
index 97258c528..81686eb5b 100644
--- a/go/sig/internal/sigcmn/common.go
+++ b/go/sig/internal/sigcmn/common.go
@@ -23,6 +23,8 @@ import (
 	"github.com/scionproto/scion/go/lib/common"
 	"github.com/scionproto/scion/go/lib/env"
 	"github.com/scionproto/scion/go/lib/log"
+	"github.com/scionproto/scion/go/lib/sciond/fake"
+	"github.com/scionproto/scion/go/lib/serrors"
 	"github.com/scionproto/scion/go/lib/snet"
 	"github.com/scionproto/scion/go/lib/sock/reliable"
 	"github.com/scionproto/scion/go/sig/internal/pathmgr"
@@ -58,26 +60,7 @@ func Init(cfg sigconfig.SigConf, sdCfg env.SciondClient) error {
 	MgmtAddr = mgmt.NewAddr(Host, cfg.CtrlPort, cfg.EncapPort)
 	encapPort = cfg.EncapPort
 
-	ds := reliable.NewDispatcher(cfg.Dispatcher)
-	// TODO(karampok). To be kept until https://github.com/scionproto/scion/issues/3377
-	wait := func() (*snet.SCIONNetwork, pathmgr.Resolver, error) {
-		deadline := time.Now().Add(sdCfg.InitialConnectPeriod.Duration)
-		var retErr error
-		for tries := 0; time.Now().Before(deadline); tries++ {
-			resolver, err := snetmigrate.ResolverFromSD(sdCfg.Path)
-			if err == nil {
-				return snet.NewNetworkWithPR(cfg.IA, ds, &snetmigrate.PathQuerier{
-					Resolver: resolver,
-					IA:       cfg.IA,
-				}, resolver), resolver, nil
-			}
-			log.Debug("SIG is retrying to get NewNetwork", err)
-			retErr = err
-			time.Sleep(time.Second)
-		}
-		return nil, nil, retErr
-	}
-	network, resolver, err := wait()
+	network, resolver, err := initNetwork(cfg, sdCfg)
 	if err != nil {
 		return common.NewBasicError("Error creating local SCION Network context", err)
 	}
@@ -94,6 +77,53 @@ func Init(cfg sigconfig.SigConf, sdCfg env.SciondClient) error {
 	return nil
 }
 
+func initNetwork(cfg sigconfig.SigConf,
+	sdCfg env.SciondClient) (*snet.SCIONNetwork, pathmgr.Resolver, error) {
+
+	if sdCfg.FakeData != "" {
+		return initNetworkWithFakeSCIOND(cfg, sdCfg)
+	}
+	return initNetworkWithRealSCIOND(cfg, sdCfg)
+}
+
+func initNetworkWithFakeSCIOND(cfg sigconfig.SigConf,
+	sdCfg env.SciondClient) (*snet.SCIONNetwork, pathmgr.Resolver, error) {
+
+	ds := reliable.NewDispatcher(cfg.Dispatcher)
+	sciondConn, err := fake.NewFromFile(sdCfg.FakeData)
+	if err != nil {
+		return nil, nil, serrors.WrapStr("unable to initialize fake SCIOND service", err)
+	}
+	pathResolver := pathmgr.New(sciondConn, pathmgr.Timers{})
+	network := snet.NewNetworkWithPR(cfg.IA, ds, &snetmigrate.PathQuerier{
+		Resolver: pathResolver,
+		IA:       cfg.IA,
+	}, pathResolver)
+	return network, pathResolver, nil
+}
+
+func initNetworkWithRealSCIOND(cfg sigconfig.SigConf,
+	sdCfg env.SciondClient) (*snet.SCIONNetwork, pathmgr.Resolver, error) {
+
+	ds := reliable.NewDispatcher(cfg.Dispatcher)
+	// TODO(karampok). To be kept until https://github.com/scionproto/scion/issues/3377
+	deadline := time.Now().Add(sdCfg.InitialConnectPeriod.Duration)
+	var retErr error
+	for tries := 0; time.Now().Before(deadline); tries++ {
+		resolver, err := snetmigrate.ResolverFromSD(sdCfg.Path)
+		if err == nil {
+			return snet.NewNetworkWithPR(cfg.IA, ds, &snetmigrate.PathQuerier{
+				Resolver: resolver,
+				IA:       cfg.IA,
+			}, resolver), resolver, nil
+		}
+		log.Debug("SIG is retrying to get NewNetwork", err)
+		retErr = err
+		time.Sleep(time.Second)
+	}
+	return nil, nil, retErr
+}
+
 func EncapSnetAddr() *snet.Addr {
 	return &snet.Addr{IA: IA, Host: &addr.AppAddr{L3: Host, L4: uint16(encapPort)}}
 }
diff --git a/go/sig/main.go b/go/sig/main.go
index 752b9f2ef..723103673 100644
--- a/go/sig/main.go
+++ b/go/sig/main.go
@@ -139,7 +139,7 @@ func validateConfig() error {
 
 func setupTun() (io.ReadWriteCloser, error) {
 	if err := checkPerms(); err != nil {
-		return nil, serrors.New("Permissions checks failed")
+		return nil, serrors.WrapStr("Permissions checks failed", err)
 	}
 	tunLink, tunIO, err := xnet.ConnectTun(cfg.Sig.Tun)
 	if err != nil {
@@ -176,7 +176,7 @@ func checkPerms() error {
 	if err != nil {
 		return common.NewBasicError("Error retrieving user", err)
 	}
-	if u.Uid == "0" {
+	if u.Uid == "0" && !cfg.Features.AllowRunAsRoot {
 		return serrors.New("Running as root is not allowed for security reasons")
 	}
 	caps, err := capability.NewPid(0)
diff --git a/go/tools/udpproxy/udpproxy.go b/go/tools/udpproxy/udpproxy.go
index 736e4650b..194b315b3 100644
--- a/go/tools/udpproxy/udpproxy.go
+++ b/go/tools/udpproxy/udpproxy.go
@@ -13,52 +13,83 @@ import (
 )
 
 var (
-	listen = flag.String("listen", "", "address to listen on (e.g., 127.0.0.1:30041) (required)")
-	to     = flag.String("to", "", "address to redirect to (e.g., 127.0.0.1:30041) (required)")
+	localX = flag.String("local_x", "",
+		"local UDP address on network x, in IP:port format  (required)")
+	remoteX = flag.String("remote_x", "",
+		"remote UDP address on network x, in IP:port format (required)")
+	localY = flag.String("local_y", "",
+		"local UDP address on network y, in IP:port format (required)")
+	remoteY = flag.String("remote_y", "",
+		"remote UDP address on network y, in IP:port format (required)")
 )
 
 func main() {
 	flag.Parse()
 
-	if err := Proxy(*listen, *to); err != nil {
+	if err := Proxy(*localX, *remoteX, *localY, *remoteY); err != nil {
 		log.Crit("Fatal proxy error", "err", err)
 		os.Exit(1)
 	}
 }
 
-func Proxy(listen, to string) error {
-	listenAddr, err := net.ResolveUDPAddr("udp", listen)
+func Proxy(localX, remoteX, localY, remoteY string) error {
+	lxAddr, err := net.ResolveUDPAddr("udp", localX)
 	if err != nil {
-		return serrors.New("unable to parse listen address", "err", err)
+		return serrors.New("unable to parse local x address", "err", err)
 	}
-
-	listenConn, err := net.ListenUDP("udp", listenAddr)
+	rxAddr, err := net.ResolveUDPAddr("udp", remoteX)
+	if err != nil {
+		return serrors.New("unable to parse remote x address", "err", err)
+	}
+	xConn, err := net.ListenUDP("udp", lxAddr)
 	if err != nil {
-		return serrors.New("unable to open listen conn", "err", err)
+		return serrors.New("unable to open x conn", "err", err)
 	}
 
-	redirectConn, err := net.Dial("udp", to)
+	lyAddr, err := net.ResolveUDPAddr("udp", localY)
+	if err != nil {
+		return serrors.New("unable to parse local y address", "err", err)
+	}
+	ryAddr, err := net.ResolveUDPAddr("udp", remoteY)
+	if err != nil {
+		return serrors.New("unable to parse remote y address", "err", err)
+	}
+	yConn, err := net.ListenUDP("udp", lyAddr)
 	if err != nil {
-		return serrors.New("unable to open conn to destination", "err", err)
+		return serrors.New("unable to open y conn", "err", err)
 	}
+
 	log.Info(
 		fmt.Sprintf(
 			"Redirecting messages received on %v to %v",
-			listenConn.LocalAddr(),
-			redirectConn.RemoteAddr(),
+			xConn.LocalAddr(),
+			remoteY,
 		),
 	)
+	log.Info(
+		fmt.Sprintf(
+			"Redirecting messages received on %v to %v",
+			yConn.LocalAddr(),
+			remoteX,
+		),
+	)
+
+	go redirect(xConn, yConn, ryAddr)
+	go redirect(yConn, xConn, rxAddr)
+	select {}
+}
 
+func redirect(from, to net.PacketConn, toAddr *net.UDPAddr) {
 	b := make([]byte, 1<<16)
 	for {
-		n, _, err := listenConn.ReadFromUDP(b)
+		n, _, err := from.ReadFrom(b)
 		if err != nil {
-			return serrors.New("unable to read from listen conn", "err", err)
+			log.Error("Unable to read from listen conn", "err", err)
 		}
 
-		_, err = redirectConn.Write(b[:n])
+		_, err = to.WriteTo(b[:n], toAddr)
 		if err != nil {
-			return serrors.New("unable to write to destination", "err", err)
+			log.Error("unable to write to destination", "err", err)
 		}
 	}
 }


Test patch:
diff --git a/acceptance/sig_failover/test b/acceptance/sig_failover/test
new file mode 100755
index 000000000..dab3b2c7a
--- /dev/null
+++ b/acceptance/sig_failover/test
@@ -0,0 +1,109 @@
+#!/bin/bash
+
+# This test checks whether path failover in SIG works as expected.
+# It sets up two ASes connected by two paths. It disables one path
+# and checks whether IP traffic is still going through. Then it
+# enables the first path and disables the other one. It tries to pass
+# IP packets between the ASes again.
+
+# Docker topology:
+#
+# +---------------------------+     +---------------------------+
+# | dispatcher1 network stack |     | dispatcher2 network stack |
+# |    +-----------------+    |     |    +-----------------+    |
+# |    |     tester1     |    |     |    |     tester2     |    |
+# |    +-----------------+    |     |    +-----------------+    |
+# |                           |     |                           |
+# |    +-----------------+    |     |    +-----------------+    |
+# |    |      sig1       |    |     |    |      sig2       |    |
+# |    +-----------------+    |     |    +-----------------+    |
+# |                           |     |                           |
+# |    +-----------------+    |     |    +-----------------+    |
+# |    |   dispatcher1   |    |     |    |   dispatcher2   |    |
+# |    |     :30041      |    |     |    |     :30041      |    |
+# |    +-----------------+    |     |    +-----------------+    |
+# |                           |     |                           |
+# |    (route via dev sig)    |     |    (route via dev sig)    |
+# |                           |     |                           |
+# |      242.254.100.2/24     |     |     242.254.200.2/24      |
+# +------------+--------------+     +-------------+-------------+
+#              |                                  |
+#   +----------+------------+         +-----------+-----------+
+#   |        bridge1        |         |       bridge2         |
+#   |    242.254.100.1/24   |         |   242.254.200.1/24    |
+#   +-+-+-------------------+         +-------------------+-+-+
+#     | |                                                 | |
+#     | | +---------------------------------------------+ | |
+#     | +-+                    patha                    +-+ |
+#     |   | 242.254.100.3:50000 <-> 242.254.200.3:50000 |   |
+#     |   +---------------------------------------------+   |
+#     |                                                     |
+#     |   +---------------------------------------------+   |
+#     +---+                    pathb                    +----
+#         | 242.254.100.3:50000 <-> 242.254.200.4:50000 |
+#         +---------------------------------------------+
+
+
+# --- begin runfiles.bash initialization v2 ---
+# Copy-pasted from the Bazel Bash runfiles library v2.
+set -uo pipefail; f=bazel_tools/tools/bash/runfiles/runfiles.bash
+source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \
+source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \
+source "$0.runfiles/$f" 2>/dev/null || \
+source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
+source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
+{ echo>&2 "ERROR: cannot find $f"; exit 1; }; f=; set -e
+# --- end runfiles.bash initialization v2 ---
+
+run_test() {(set -e
+    # Register with the docker daemon the docker images bazel created
+    # (--norun is used for docker images created via go_image because the script runs them by default)
+    bash "$(rlocation __main__/go/tools/udpproxy/udpproxy)" --norun
+    bash "$(rlocation __main__/acceptance/sig_failover/dispatcher1)"
+    bash "$(rlocation __main__/acceptance/sig_failover/dispatcher2)"
+    bash "$(rlocation __main__/acceptance/sig_failover/sig1)"
+    bash "$(rlocation __main__/acceptance/sig_failover/sig2)"
+
+    docker-compose -f acceptance/sig_failover/docker-compose.yml up -d dispatcher1 dispatcher2 sig1 sig2
+
+    # Set up forward route on network stack 1 and 2 through sig tunnel device
+    # If this fails, the test is not stopped.
+    docker-compose -f acceptance/sig_failover/docker-compose.yml run --rm tester1 ip route add 242.254.200.2/32 dev sig || true
+    docker-compose -f acceptance/sig_failover/docker-compose.yml run --rm tester2 ip route add 242.254.100.2/32 dev sig || true
+
+    echo "Initial ping tests"
+    docker-compose -f acceptance/sig_failover/docker-compose.yml up -d patha pathb
+    docker-compose -f acceptance/sig_failover/docker-compose.yml run --rm tester1 ping -c 3 242.254.200.2
+
+    echo "Shutting down path A"
+    docker-compose -f acceptance/sig_failover/docker-compose.yml stop patha
+    echo "Check pings still work"
+    docker-compose -f acceptance/sig_failover/docker-compose.yml run --rm tester1 ping -c 3 242.254.200.2
+
+    echo "Shutting down path B, bringing up path A"
+    docker-compose -f acceptance/sig_failover/docker-compose.yml stop pathb
+    docker-compose -f acceptance/sig_failover/docker-compose.yml start patha
+    echo "Check pings still work"
+    docker-compose -f acceptance/sig_failover/docker-compose.yml run --rm tester1 ping -c 3 242.254.200.2
+
+    echo "Bringing up bath B"
+    docker-compose -f acceptance/sig_failover/docker-compose.yml start pathb
+    echo "Check pings still work"
+    docker-compose -f acceptance/sig_failover/docker-compose.yml run --rm tester1 ping -c 3 242.254.200.2
+)}
+
+set +e
+run_test
+RC=$?
+
+# Write outputs to bazel directory
+OUTPUT_DIR=$TEST_UNDECLARED_OUTPUTS_DIR
+mkdir -p $OUTPUT_DIR/logs
+
+for FILE in sig1-ff00_0_110.log sig1-ff00_0_111.log disp_1-ff00_0_110.log disp_1-ff00_0_111.log; do
+    docker-compose -f acceptance/sig_failover/docker-compose.yml run log_exporter cat /share/logs/$FILE > $OUTPUT_DIR/logs/$FILE
+done
+
+docker-compose -f acceptance/sig_failover/docker-compose.yml down
+
+exit $RC
diff --git a/acceptance/sig_failover/testdata/1-ff00_0_110/dispatcher/disp.toml b/acceptance/sig_failover/testdata/1-ff00_0_110/dispatcher/disp.toml
new file mode 100644
index 000000000..b7f555002
--- /dev/null
+++ b/acceptance/sig_failover/testdata/1-ff00_0_110/dispatcher/disp.toml
@@ -0,0 +1,12 @@
+[dispatcher]
+ID = "disp_1-ff00_0_110"
+
+[features]
+AllowRunAsRoot = true
+
+[logging.console]
+Level = "crit"
+
+[logging.file]
+Level = "trace"
+Path = "/share/logs/disp_1-ff00_0_110.log"
diff --git a/acceptance/sig_failover/testdata/1-ff00_0_110/sig/cfg.json b/acceptance/sig_failover/testdata/1-ff00_0_110/sig/cfg.json
new file mode 100644
index 000000000..90010c0ca
--- /dev/null
+++ b/acceptance/sig_failover/testdata/1-ff00_0_110/sig/cfg.json
@@ -0,0 +1,10 @@
+{
+  "ConfigVersion": 1,
+  "ASes": {
+    "1-ff00:0:111": {
+      "Nets": [
+        "242.254.200.0/24"
+      ]
+    }
+  }
+}
diff --git a/acceptance/sig_failover/testdata/1-ff00_0_110/sig/fake_sciond.json b/acceptance/sig_failover/testdata/1-ff00_0_110/sig/fake_sciond.json
new file mode 100644
index 000000000..71471eb83
--- /dev/null
+++ b/acceptance/sig_failover/testdata/1-ff00_0_110/sig/fake_sciond.json
@@ -0,0 +1,19 @@
+{
+    "entries": [
+        {
+            "seconds": 0,
+            "paths": [
+                {
+                    "fingerprint": "PathAAAA",
+                    "next_hop": "242.254.100.3:50000",
+                    "ia": "1-ff00:0:111"
+                },
+                {
+                    "fingerprint": "PathBBBB",
+                    "next_hop": "242.254.100.4:50000",
+                    "ia": "1-ff00:0:111"
+                }
+            ]
+        }
+    ]
+}
diff --git a/acceptance/sig_failover/testdata/1-ff00_0_110/sig/sig.toml b/acceptance/sig_failover/testdata/1-ff00_0_110/sig/sig.toml
new file mode 100644
index 000000000..64a45021f
--- /dev/null
+++ b/acceptance/sig_failover/testdata/1-ff00_0_110/sig/sig.toml
@@ -0,0 +1,20 @@
+[sig]
+ID = "sig1-ff00_0_110"
+SIGConfig = "/cfg.json"
+IA = "1-ff00:0:110"
+IP = "242.254.100.2"
+
+[features]
+AllowRunAsRoot = true
+
+[sd_client]
+FakeData = "/fake_sciond.json"
+Path = "/run/shm/sciond/sd1-ff00_0_110.sock"
+
+[logging.console]
+Level = "crit"
+
+[logging.file]
+FlushInterval = 1
+Level = "trace"
+Path = "/share/logs/sig1-ff00_0_110.log"
diff --git a/acceptance/sig_failover/testdata/1-ff00_0_111/dispatcher/disp.toml b/acceptance/sig_failover/testdata/1-ff00_0_111/dispatcher/disp.toml
new file mode 100644
index 000000000..2914c73c0
--- /dev/null
+++ b/acceptance/sig_failover/testdata/1-ff00_0_111/dispatcher/disp.toml
@@ -0,0 +1,12 @@
+[dispatcher]
+ID = "disp_1-ff00_0_111"
+
+[features]
+AllowRunAsRoot = true
+
+[logging.console]
+Level = "crit"
+
+[logging.file]
+Level = "trace"
+Path = "/share/logs/disp_1-ff00_0_111.log"
diff --git a/acceptance/sig_failover/testdata/1-ff00_0_111/sig/cfg.json b/acceptance/sig_failover/testdata/1-ff00_0_111/sig/cfg.json
new file mode 100644
index 000000000..0f4fa69e7
--- /dev/null
+++ b/acceptance/sig_failover/testdata/1-ff00_0_111/sig/cfg.json
@@ -0,0 +1,10 @@
+{
+  "ConfigVersion": 1,
+  "ASes": {
+    "1-ff00:0:110": {
+      "Nets": [
+        "242.254.100.0/24"
+      ]
+    }
+  }
+}
diff --git a/acceptance/sig_failover/testdata/1-ff00_0_111/sig/fake_sciond.json b/acceptance/sig_failover/testdata/1-ff00_0_111/sig/fake_sciond.json
new file mode 100644
index 000000000..a851e5d15
--- /dev/null
+++ b/acceptance/sig_failover/testdata/1-ff00_0_111/sig/fake_sciond.json
@@ -0,0 +1,19 @@
+{
+    "entries": [
+        {
+            "seconds": 0,
+            "paths": [
+                {
+                    "fingerprint": "PathAAAA",
+                    "next_hop": "242.254.200.3:50000",
+                    "ia": "1-ff00:0:110"
+                },
+                {
+                    "fingerprint": "PathBBBB",
+                    "next_hop": "242.254.200.4:50000",
+                    "ia": "1-ff00:0:110"
+                }
+            ]
+        }
+    ]
+}
diff --git a/acceptance/sig_failover/testdata/1-ff00_0_111/sig/sig.toml b/acceptance/sig_failover/testdata/1-ff00_0_111/sig/sig.toml
new file mode 100644
index 000000000..b74fbc825
--- /dev/null
+++ b/acceptance/sig_failover/testdata/1-ff00_0_111/sig/sig.toml
@@ -0,0 +1,20 @@
+[sig]
+ID = "sig1-ff00_0_111"
+SIGConfig = "/cfg.json"
+IA = "1-ff00:0:111"
+IP = "242.254.200.2"
+
+[features]
+AllowRunAsRoot = true
+
+[sd_client]
+FakeData = "/fake_sciond.json"
+Path = "/run/shm/sciond/sd1-ff00_0_111.sock"
+
+[logging.console]
+Level = "crit"
+
+[logging.file]
+FlushInterval = 1
+Level = "trace"
+Path = "/share/logs/sig1-ff00_0_111.log"
diff --git a/acceptance/sig_failover_acceptance/Test.topo b/acceptance/sig_failover_acceptance/Test.topo
deleted file mode 100644
index ce2ac2777..000000000
--- a/acceptance/sig_failover_acceptance/Test.topo
+++ /dev/null
@@ -1,14 +0,0 @@
---- # Topology for sig_failover_acceptance test.
-ASes:
-  "1-ff00:0:110":
-    core: true
-    mtu: 1400
-  "1-ff00:0:111":
-    cert_issuer: 1-ff00:0:110
-links:
-  - {a: "1-ff00:0:110#1", b: "1-ff00:0:111#1", linkAtoB: CHILD}
-  - {a: "1-ff00:0:110#2", b: "1-ff00:0:111#2", linkAtoB: CHILD}
-CAs:
-  CA1-1:
-    ISD: 1
-    commonName: CA1-1
diff --git a/acceptance/sig_failover_acceptance/test b/acceptance/sig_failover_acceptance/test
deleted file mode 100755
index 1cce5d89b..000000000
--- a/acceptance/sig_failover_acceptance/test
+++ /dev/null
@@ -1,47 +0,0 @@
-#!/bin/bash
-
-# This test checks whether path failover in SIG works as expected.
-# It sets up two ASes connected by two paths. It disables one path
-# and checks whether IP traffic is still going through. Then it
-# enables the first path and disables the other one. It tries to pass
-# IP packets between the ASes again.
-
-PROGRAM=`basename "$0"`
-COMMAND="$1"
-TEST_NAME="sig_failover"
-TEST_TOPOLOGY="acceptance/sig_failover_acceptance/Test.topo"
-
-. acceptance/sigutil/common.sh
-
-test_run() {
-    set -e
-    log "Initial ping tests"
-    # Make sure that pings go through in a vanilla setup.
-    ./bin/sig_ping_acceptance -d -log.console info
-    # Disable one of the two paths between the ASes by stopping the
-    # corresponding BR.
-    log "Stopping br-110-2"
-    ./tools/dc stop scion_br1-ff00_0_110-2
-    # Make sure that the pings still get through.
-    log "Check pings still work"
-    ./bin/sig_ping_acceptance -d -log.console info
-    # Enable the disabled path, disable the other path.
-    log "Start br-110-2"
-    ./tools/dc start scion_br1-ff00_0_110-2
-    # Make sure the revocation of 110-2 is done.
-    sleep 10
-    log "Stop br-110-1"
-    ./tools/dc stop scion_br1-ff00_0_110-1
-    # Make sure that the pings still get through.
-    log "Check pings still work"
-    ./bin/sig_ping_acceptance -d -log.console info
-    # Enable both paths again.
-    log "Start br-110-1"
-    ./tools/dc start scion_br1-ff00_0_110-1
-    # Make sure that the pings still get through.
-    log "Final ping check"
-    ./bin/sig_ping_acceptance -d -log.console info
-}
-
-shift
-do_command $PROGRAM $COMMAND $TEST_NAME "$@"
diff --git a/go/lib/sciond/fake/fake_test.go b/go/lib/sciond/fake/fake_test.go
new file mode 100644
index 000000000..fa37d486f
--- /dev/null
+++ b/go/lib/sciond/fake/fake_test.go
@@ -0,0 +1,261 @@
+// Copyright 2019 Anapaya Systems
+
+package fake_test
+
+import (
+	"context"
+	"encoding/json"
+	"flag"
+	"io/ioutil"
+	"net"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/scionproto/scion/go/lib/addr"
+	"github.com/scionproto/scion/go/lib/sciond"
+	"github.com/scionproto/scion/go/lib/sciond/fake"
+	"github.com/scionproto/scion/go/lib/snet"
+	"github.com/scionproto/scion/go/lib/xtest"
+)
+
+var update = flag.Bool("update", false, "set to true to update golden files")
+
+func TestJSONConversion(t *testing.T) {
+	script := &fake.Script{
+		Entries: []*fake.Entry{
+			{
+				Seconds: 0,
+				Paths: []*fake.Path{
+					{
+						JSONFingerprint: "Foo",
+						JSONNextHop: &fake.UDPAddr{
+							IP:   net.IP{192, 168, 0, 1},
+							Port: 80,
+						},
+						JSONIA: xtest.MustParseIA("1-ff00:0:1"),
+					},
+				},
+			},
+		},
+	}
+
+	bytes, err := json.MarshalIndent(script, "  ", "  ")
+	require.NoError(t, err)
+	bytes = append(bytes, '\n')
+	if *update {
+		err = ioutil.WriteFile("testdata/sd.json", bytes, 0644)
+		require.NoError(t, err)
+	}
+
+	loadedBytes, err := ioutil.ReadFile("testdata/sd.json")
+	require.NoError(t, err)
+	assert.Equal(t, bytes, loadedBytes)
+
+	var loadedScript fake.Script
+	err = json.Unmarshal(loadedBytes, &loadedScript)
+	require.NoError(t, err)
+
+	assert.Equal(t, script, &loadedScript)
+}
+
+func TestPaths(t *testing.T) {
+	script := &fake.Script{
+		Entries: []*fake.Entry{
+			{
+				Seconds: 0,
+				Paths: []*fake.Path{
+					{
+						JSONFingerprint: "Foo",
+						JSONNextHop: &fake.UDPAddr{
+							IP:   net.IP{10, 0, 0, 1},
+							Port: 80,
+						},
+						JSONIA: xtest.MustParseIA("1-ff00:0:1"),
+					},
+				},
+			},
+			{
+				Seconds: 1,
+				Paths: []*fake.Path{
+					{
+						JSONFingerprint: "Foo2",
+						JSONNextHop: &fake.UDPAddr{
+							IP:   net.IP{10, 0, 0, 2},
+							Port: 80,
+						},
+						JSONIA: xtest.MustParseIA("2-ff00:0:2"),
+					},
+				},
+			},
+		},
+	}
+	c := fake.New(script)
+
+	paths, err := c.Paths(
+		context.Background(),
+		xtest.MustParseIA("1-ff00:0:1"),
+		xtest.MustParseIA("1-ff00:0:2"),
+		5,
+		sciond.PathReqFlags{},
+	)
+	require.NoError(t, err)
+
+	require.Equal(t, 1, len(paths))
+	assert.Equal(t, "Foo", string(paths[0].Fingerprint()))
+	assert.Equal(t, &net.UDPAddr{IP: net.IP{10, 0, 0, 1}, Port: 80}, paths[0].OverlayNextHop())
+	assert.Equal(t, fake.DummyPath(), paths[0].Path())
+	assert.Equal(t, []snet.PathInterface{}, paths[0].Interfaces())
+	assert.Equal(t, xtest.MustParseIA("1-ff00:0:1"), paths[0].Destination())
+	assert.Equal(t, uint16(1500), paths[0].MTU())
+	// path valid for more than an hour
+	assert.True(t, paths[0].Expiry().After(time.Now().Add(time.Hour)))
+
+	time.Sleep(time.Second)
+
+	paths, err = c.Paths(
+		context.Background(),
+		xtest.MustParseIA("1-ff00:0:1"),
+		xtest.MustParseIA("1-ff00:0:2"),
+		5,
+		sciond.PathReqFlags{},
+	)
+	require.NoError(t, err)
+
+	require.Equal(t, 1, len(paths))
+	assert.Equal(t, "Foo2", string(paths[0].Fingerprint()))
+	assert.Equal(t, &net.UDPAddr{IP: net.IP{10, 0, 0, 2}, Port: 80}, paths[0].OverlayNextHop())
+	assert.Equal(t, fake.DummyPath(), paths[0].Path())
+	assert.Equal(t, []snet.PathInterface{}, paths[0].Interfaces())
+	assert.Equal(t, xtest.MustParseIA("2-ff00:0:2"), paths[0].Destination())
+	assert.Equal(t, uint16(1500), paths[0].MTU())
+	// path valid for more than an hour
+	assert.True(t, paths[0].Expiry().After(time.Now().Add(time.Hour)))
+}
+
+func TestPathCopy(t *testing.T) {
+	path := fake.Path{
+		JSONFingerprint: "foo",
+		JSONNextHop: &fake.UDPAddr{
+			IP:   net.IP{192, 168, 0, 1},
+			Port: 80,
+			Zone: "abc",
+		},
+		JSONIA: xtest.MustParseIA("1-ff00:0:1"),
+	}
+
+	copyPath := path.Copy()
+	assert.Equal(t, &path, copyPath.(*fake.Path))
+
+	path.JSONNextHop.Port = 8080
+	path.JSONNextHop.IP[3] = 1
+
+	assert.Equal(t, 80, copyPath.(*fake.Path).JSONNextHop.Port)
+	assert.Equal(t, net.IP{192, 168, 0, 1}, copyPath.(*fake.Path).JSONNextHop.IP)
+}
+
+func TestASInfo(t *testing.T) {
+	c := fake.New(&fake.Script{})
+	assert.PanicsWithValue(t, "not implemented", func() { c.ASInfo(nil, addr.IA{}) })
+}
+
+func TestIFInfo(t *testing.T) {
+	c := fake.New(&fake.Script{})
+	assert.PanicsWithValue(t, "not implemented", func() { c.IFInfo(nil, nil) })
+}
+
+func TestSVCInfo(t *testing.T) {
+	c := fake.New(&fake.Script{})
+	assert.PanicsWithValue(t, "not implemented", func() { c.SVCInfo(nil, nil) })
+
+}
+
+func TestRevNotificationFromRaw(t *testing.T) {
+	c := fake.New(&fake.Script{})
+	assert.PanicsWithValue(t, "not implemented", func() { c.RevNotificationFromRaw(nil, nil) })
+}
+
+func TestRevNotification(t *testing.T) {
+	c := fake.New(&fake.Script{})
+	assert.PanicsWithValue(t, "not implemented", func() { c.RevNotification(nil, nil) })
+}
+
+func TestClose(t *testing.T) {
+	c := fake.New(&fake.Script{})
+	assert.NoError(t, c.Close(nil))
+}
+
+func TestUDPAddrMarshalText(t *testing.T) {
+	testCases := []struct {
+		Name         string
+		InputAddress *fake.UDPAddr
+		OutputText   []byte
+	}{
+		{
+			Name: "ipv4",
+			InputAddress: &fake.UDPAddr{
+				IP:   net.IP{10, 0, 0, 1},
+				Port: 80,
+			},
+			OutputText: []byte("10.0.0.1:80"),
+		},
+		{
+			Name: "ipv6",
+			InputAddress: &fake.UDPAddr{
+				IP:   net.IPv6loopback,
+				Port: 80,
+			},
+			OutputText: []byte("[::1]:80"),
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.Name, func(t *testing.T) {
+			outputText, err := tc.InputAddress.MarshalText()
+			assert.NoError(t, err)
+			assert.Equal(t, tc.OutputText, outputText)
+		})
+	}
+}
+
+func TestUDPAddrUnmarshalText(t *testing.T) {
+	testCases := []struct {
+		Name          string
+		InputText     []byte
+		OutputAddress fake.UDPAddr
+		OutputError   assert.ErrorAssertionFunc
+	}{
+		{
+			Name:      "ipv4",
+			InputText: []byte("10.0.0.1:80"),
+			OutputAddress: fake.UDPAddr{
+				IP:   net.IP{10, 0, 0, 1},
+				Port: 80,
+			},
+			OutputError: assert.NoError,
+		},
+		{
+			Name:      "ipv6",
+			InputText: []byte("[::1]:80"),
+			OutputAddress: fake.UDPAddr{
+				IP:   net.IPv6loopback,
+				Port: 80,
+			},
+			OutputError: assert.NoError,
+		},
+		{
+			Name:        "parse error",
+			InputText:   []byte("foo"),
+			OutputError: assert.Error,
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.Name, func(t *testing.T) {
+			var outputAddress fake.UDPAddr
+			err := outputAddress.UnmarshalText(tc.InputText)
+			tc.OutputError(t, err)
+			assert.Equal(t, tc.OutputAddress, outputAddress)
+		})
+	}
+}
diff --git a/go/lib/sciond/fake/testdata/sd.json b/go/lib/sciond/fake/testdata/sd.json
new file mode 100644
index 000000000..33dc08391
--- /dev/null
+++ b/go/lib/sciond/fake/testdata/sd.json
@@ -0,0 +1,14 @@
+{
+    "entries": [
+      {
+        "seconds": 0,
+        "paths": [
+          {
+            "fingerprint": "Foo",
+            "next_hop": "192.168.0.1:80",
+            "ia": "1-ff00:0:1"
+          }
+        ]
+      }
+    ]
+  }