Repository: scionproto/scion
Pull request #3561 (2019-12-27 15:55:57)

Convert sig_short_exp acceptance to use Bazel
This reduces the worst case running time of the test from
about 8 minutes to about 30 seconds.

Also:
- sig_short_exp no longer runs a control plane, instead
using a fake sciond
- removes the old sig_short_exp acceptance test from
the main acceptance pipeline

<!-- 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/3561)
<!-- Reviewable:end -->



Patch:
diff --git a/.buildkite/pipeline_buildlint.yml b/.buildkite/pipeline_buildlint.yml
index 587f0c0b4..4fdce619a 100644
--- a/.buildkite/pipeline_buildlint.yml
+++ b/.buildkite/pipeline_buildlint.yml
@@ -64,3 +64,15 @@ steps:
         - exit_status: 255 # Forced agent shutdown
     artifact_paths:
       - "artifacts.out/**/*"
+  - label: "Acceptance: sig_short_exp_time"
+    command:
+      - mkdir -p $$ACCEPTANCE_ARTIFACTS
+      - bazel test --action_env=ACCEPTANCE_ARTIFACTS //acceptance/sig_short_exp_time:sig_short_exp_time_test
+    key: sig_short_exp_time
+    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
index 918fa6c3a..af624e371 100644
--- a/acceptance/sig_failover/BUILD.bazel
+++ b/acceptance/sig_failover/BUILD.bazel
@@ -3,7 +3,6 @@ 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",
diff --git a/acceptance/sig_short_exp_time/BUILD.bazel b/acceptance/sig_short_exp_time/BUILD.bazel
new file mode 100644
index 000000000..35771769f
--- /dev/null
+++ b/acceptance/sig_short_exp_time/BUILD.bazel
@@ -0,0 +1,58 @@
+load("@io_bazel_rules_docker//container:container.bzl", "container_image")
+
+sh_test(
+    name = "sig_short_exp_time_test",
+    size = "small",
+    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_short_exp_time/docker-compose.yml b/acceptance/sig_short_exp_time/docker-compose.yml
new file mode 100644
index 000000000..9fb424e59
--- /dev/null
+++ b/acceptance/sig_short_exp_time/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_short_exp_time: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_short_exp_time: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_short_exp_time: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_short_exp_time: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/go/lib/sciond/fake/fake.go b/go/lib/sciond/fake/fake.go
index 0caee798e..54e2570cf 100644
--- a/go/lib/sciond/fake/fake.go
+++ b/go/lib/sciond/fake/fake.go
@@ -21,11 +21,21 @@ import (
 )
 
 // New creates a new fake SCIOND implementation using the data in the script.
+//
+// New also initializes path expiry times according to the expiry seconds in the script.
 func New(script *Script) sciond.Connector {
-	return &connector{
+	c := &connector{
 		script:       script,
 		creationTime: time.Now(),
 	}
+	for _, entry := range script.Entries {
+		for _, path := range entry.Paths {
+			path.creationTime = c.creationTime
+			lifetime := time.Duration(path.JSONExpirationTimestamp) * time.Second
+			path.expirationTime = path.creationTime.Add(lifetime)
+		}
+	}
+	return c
 }
 
 // NewFromFile creates a new fake SCIOND implementation using the JSON representation in the file.
@@ -48,11 +58,11 @@ type Script struct {
 
 // 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"`
+	// ReplyStartTimestamp 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)
+	ReplyStartTimestamp int `json:"reply_start_timestamp"`
 	// Paths contains the paths for a fake SCIOND reply.
 	Paths []*Path `json:"paths"`
 }
@@ -61,6 +71,15 @@ type Path struct {
 	JSONFingerprint string   `json:"fingerprint"`
 	JSONNextHop     *UDPAddr `json:"next_hop,omitempty"`
 	JSONIA          addr.IA  `json:"ia"`
+	// JSONExpirationTimestamp contains the point in time when the path expires, in seconds,
+	// relative to the time of fake connector creation. Negative timestamps are also supported, and
+	// would mean SCIOND served a path that expired in the past.
+	JSONExpirationTimestamp int `json:"expiration_timestamp"`
+
+	// creationTime contains the time when this object was constructed.
+	creationTime time.Time
+	// expirationTime contains the time when this path expires.
+	expirationTime time.Time
 }
 
 func (p Path) Fingerprint() snet.PathFingerprint {
@@ -96,7 +115,7 @@ func (p Path) MTU() uint16 {
 }
 
 func (p Path) Expiry() time.Time {
-	return time.Now().Add(time.Hour * 24 * 7 * 365 * 2) // 2 years
+	return p.expirationTime
 }
 
 func (p Path) Copy() snet.Path {
@@ -107,7 +126,10 @@ func (p Path) Copy() snet.Path {
 			Port: p.JSONNextHop.Port,
 			Zone: p.JSONNextHop.Zone,
 		},
-		JSONIA: p.JSONIA,
+		JSONIA:                  p.JSONIA,
+		JSONExpirationTimestamp: p.JSONExpirationTimestamp,
+		creationTime:            p.creationTime,
+		expirationTime:          p.expirationTime,
 	}
 }
 
@@ -151,7 +173,7 @@ func (c connector) Paths(_ context.Context, _, _ addr.IA, max uint16,
 	var entry *Entry
 	for i := 0; i < len(c.script.Entries); i++ {
 		entry = c.script.Entries[i]
-		if secondsElapsed <= entry.Seconds {
+		if secondsElapsed <= entry.ReplyStartTimestamp {
 			break
 		}
 	}
@@ -162,13 +184,13 @@ func (c connector) Paths(_ context.Context, _, _ addr.IA, max uint16,
 	if intMax > len(entry.Paths) {
 		intMax = len(entry.Paths)
 	}
-	return adapter(entry.Paths[:intMax]), nil
+	return c.adapter(entry.Paths[:intMax]), nil
 }
 
-func adapter(paths []*Path) []snet.Path {
+func (c connector) adapter(paths []*Path) []snet.Path {
 	var snetPaths []snet.Path
 	for _, path := range paths {
-		snetPaths = append(snetPaths, path)
+		snetPaths = append(snetPaths, path.Copy())
 	}
 	return snetPaths
 }


Test patch:
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
index 71471eb83..9814abb3b 100644
--- 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
@@ -1,17 +1,19 @@
 {
     "entries": [
         {
-            "seconds": 0,
+            "reply_start_timestamp": 0,
             "paths": [
                 {
                     "fingerprint": "PathAAAA",
                     "next_hop": "242.254.100.3:50000",
-                    "ia": "1-ff00:0:111"
+                    "ia": "1-ff00:0:111",
+                    "expiration_timestamp": 7200
                 },
                 {
                     "fingerprint": "PathBBBB",
                     "next_hop": "242.254.100.4:50000",
-                    "ia": "1-ff00:0:111"
+                    "ia": "1-ff00:0:111",
+                    "expiration_timestamp": 7200
                 }
             ]
         }
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
index a851e5d15..af8ee0bdf 100644
--- 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
@@ -1,17 +1,19 @@
 {
     "entries": [
         {
-            "seconds": 0,
+            "reply_start_timestamp": 0,
             "paths": [
                 {
                     "fingerprint": "PathAAAA",
                     "next_hop": "242.254.200.3:50000",
-                    "ia": "1-ff00:0:110"
+                    "ia": "1-ff00:0:110",
+                    "expiration_timestamp": 7200
                 },
                 {
                     "fingerprint": "PathBBBB",
                     "next_hop": "242.254.200.4:50000",
-                    "ia": "1-ff00:0:110"
+                    "ia": "1-ff00:0:110",
+                    "expiration_timestamp": 7200
                 }
             ]
         }
diff --git a/acceptance/sig_short_exp_time/test b/acceptance/sig_short_exp_time/test
new file mode 100755
index 000000000..a60d96f61
--- /dev/null
+++ b/acceptance/sig_short_exp_time/test
@@ -0,0 +1,118 @@
+#!/bin/bash
+
+# This test checks whether the SIG quickly switches paths when one is close to
+# expiration.
+#
+# The test sets up two ASes connected by two paths. The first SCIOND path reply
+# only contains the first path, and it has a very short lifetime. Path replies
+# after 5 seconds have passed include both paths: the first path with the short
+# lifetime, and the second path with the long lifetime. Immediately after the
+# short lifetime expires, the first path is taken down and a ping sent out. If
+# the failover was correct, this ping should not be lost.
+
+# 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_short_exp_time/dispatcher1)"
+    bash "$(rlocation __main__/acceptance/sig_short_exp_time/dispatcher2)"
+    bash "$(rlocation __main__/acceptance/sig_short_exp_time/sig1)"
+    bash "$(rlocation __main__/acceptance/sig_short_exp_time/sig2)"
+
+    docker-compose -f acceptance/sig_short_exp_time/docker-compose.yml up -d dispatcher1 dispatcher2 sig1 sig2 patha pathb
+
+    # Set up forward route on network stack 1 and 2 through the sig tunnel
+    # device. The route is a property of the network stack, and persists after
+    # the container that added it has exited.
+    #
+    # If the route configuration fails, the test is not stopped.
+    docker-compose -f acceptance/sig_short_exp_time/docker-compose.yml run --name route1 --rm tester1 ip route add 242.254.200.2/32 dev sig || true
+    docker-compose -f acceptance/sig_short_exp_time/docker-compose.yml run --name route2 --rm tester2 ip route add 242.254.100.2/32 dev sig || true
+
+    echo "Start background ping, ping every 0.2 seconds"
+    docker-compose -f acceptance/sig_short_exp_time/docker-compose.yml run --name tester1 -d tester1 ping -i 0.2 242.254.200.2
+
+    echo "Waiting 10 seconds for path A to expire..."
+    sleep 10
+    echo "Path A expired, simulating it by shutting down path A proxy"
+    # Traffic should have switched beforehand to path b, and no pings should be lost
+    docker-compose -f acceptance/sig_short_exp_time/docker-compose.yml stop patha
+    sleep 1
+    docker kill -s SIGINT tester1
+
+    sleep 1
+    LINE=`docker logs tester1 | tail -n 2 | head -n 1`
+    echo "Ping results: $LINE"
+
+    ATTEMPTS=`echo $LINE | cut -d ' ' -f 1`
+    SUCCESSES=`echo $LINE | cut -d ' ' -f 4`
+    if [ $ATTEMPTS -ne $SUCCESSES ]; then
+        echo "Test failed, attempted $ATTEMPTS pings, but only $SUCCESSES succeeded"
+        return 1
+    fi
+)}
+
+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_short_exp_time/docker-compose.yml run log_exporter cat /share/logs/$FILE > $OUTPUT_DIR/logs/$FILE
+done
+
+docker-compose -f acceptance/sig_short_exp_time/docker-compose.yml down
+
+exit $RC
diff --git a/acceptance/sig_short_exp_time/testdata/1-ff00_0_110/dispatcher/disp.toml b/acceptance/sig_short_exp_time/testdata/1-ff00_0_110/dispatcher/disp.toml
new file mode 100644
index 000000000..b7f555002
--- /dev/null
+++ b/acceptance/sig_short_exp_time/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_short_exp_time/testdata/1-ff00_0_110/sig/cfg.json b/acceptance/sig_short_exp_time/testdata/1-ff00_0_110/sig/cfg.json
new file mode 100644
index 000000000..90010c0ca
--- /dev/null
+++ b/acceptance/sig_short_exp_time/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_short_exp_time/testdata/1-ff00_0_110/sig/fake_sciond.json b/acceptance/sig_short_exp_time/testdata/1-ff00_0_110/sig/fake_sciond.json
new file mode 100644
index 000000000..5a34814ad
--- /dev/null
+++ b/acceptance/sig_short_exp_time/testdata/1-ff00_0_110/sig/fake_sciond.json
@@ -0,0 +1,32 @@
+{
+    "entries": [
+        {
+            "reply_start_timestamp": 0,
+            "paths": [
+                {
+                    "fingerprint": "PathAAAAExpiresSoon",
+                    "next_hop": "242.254.100.3:50000",
+                    "ia": "1-ff00:0:111",
+                    "expiration_timestamp": 10
+                }
+            ]
+        },
+        {
+            "reply_start_timestamp": 5,
+            "paths": [
+                {
+                    "fingerprint": "PathAAAAExpiresSoon",
+                    "next_hop": "242.254.100.3:50000",
+                    "ia": "1-ff00:0:111",
+                    "expiration_timestamp": 10
+                },
+                {
+                    "fingerprint": "PathBBBBExpiresLate",
+                    "next_hop": "242.254.100.4:50000",
+                    "ia": "1-ff00:0:111",
+                    "expiration_timestamp": 7200
+                }
+            ]
+        }
+    ]
+}
diff --git a/acceptance/sig_short_exp_time/testdata/1-ff00_0_110/sig/sig.toml b/acceptance/sig_short_exp_time/testdata/1-ff00_0_110/sig/sig.toml
new file mode 100644
index 000000000..64a45021f
--- /dev/null
+++ b/acceptance/sig_short_exp_time/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_short_exp_time/testdata/1-ff00_0_111/dispatcher/disp.toml b/acceptance/sig_short_exp_time/testdata/1-ff00_0_111/dispatcher/disp.toml
new file mode 100644
index 000000000..2914c73c0
--- /dev/null
+++ b/acceptance/sig_short_exp_time/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_short_exp_time/testdata/1-ff00_0_111/sig/cfg.json b/acceptance/sig_short_exp_time/testdata/1-ff00_0_111/sig/cfg.json
new file mode 100644
index 000000000..0f4fa69e7
--- /dev/null
+++ b/acceptance/sig_short_exp_time/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_short_exp_time/testdata/1-ff00_0_111/sig/fake_sciond.json b/acceptance/sig_short_exp_time/testdata/1-ff00_0_111/sig/fake_sciond.json
new file mode 100644
index 000000000..03e237784
--- /dev/null
+++ b/acceptance/sig_short_exp_time/testdata/1-ff00_0_111/sig/fake_sciond.json
@@ -0,0 +1,32 @@
+{
+    "entries": [
+        {
+            "seconds": 0,
+            "paths": [
+                {
+                    "fingerprint": "PathAAAAExpiresSoon",
+                    "next_hop": "242.254.200.3:50000",
+                    "ia": "1-ff00:0:110",
+                    "expiry_seconds": 10
+                }
+            ]
+        },
+        {
+            "reply_start_timestamp": 5,
+            "paths": [
+                {
+                    "fingerprint": "PathAAAAExpiresSoon",
+                    "next_hop": "242.254.200.3:50000",
+                    "ia": "1-ff00:0:110",
+                    "expiration_timestamp": 10
+                },
+                {
+                    "fingerprint": "PathBBBBExpiresLate",
+                    "next_hop": "242.254.200.4:50000",
+                    "ia": "1-ff00:0:110",
+                    "expiration_timestamp": 7200
+                }
+            ]
+        }
+    ]
+}
diff --git a/acceptance/sig_short_exp_time/testdata/1-ff00_0_111/sig/sig.toml b/acceptance/sig_short_exp_time/testdata/1-ff00_0_111/sig/sig.toml
new file mode 100644
index 000000000..b74fbc825
--- /dev/null
+++ b/acceptance/sig_short_exp_time/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_short_exp_time_acceptance/test b/acceptance/sig_short_exp_time_acceptance/test
deleted file mode 100755
index 1075c33c0..000000000
--- a/acceptance/sig_short_exp_time_acceptance/test
+++ /dev/null
@@ -1,109 +0,0 @@
-#!/usr/bin/env python3
-
-# Copyright 2019 Anapaya Systems
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#   http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-
-import logging
-import os
-import sys
-import yaml
-from datetime import datetime, timedelta
-
-from plumbum import cli
-from plumbum import local
-from plumbum.path.local import LocalPath
-
-from lib.util import write_file
-from acceptance.common.log import LogExec, init_log
-from acceptance.common.base import CmdBase, TestBase, set_name
-
-
-set_name(__file__)
-logger = logging.getLogger(__name__)
-
-
-class Test(TestBase):
-    """
-    Test that the SIG switches to non-expired paths.
-
-    In the setup phase, this test sets up the tiny topology. The beacon servers
-    are configured to only construct paths with the smallest expiration time
-    (5m37s).
-
-    In the run phase, `sig_ping_acceptance` is constantly called for 6 minutes.
-    The test is successful, if the pings do not fail during this time.
-
-    To disable this test, set the `DISABLE_SHORT_EXPIRATION` env variable.
-    """
-
-class Base(CmdBase):
-    enabled = True
-
-    @cli.switch('disable-short-exp', envname='DISABLE_SHORT_EXPIRATION',
-                help='disable this test')
-    def enable_short_exp(self):
-        self.enabled = False
-
-
-@Test.subcommand('setup')
-class TestSetup(Base):
-
-    @LogExec(logger, 'setup')
-    def main(self):
-        if not self.enabled:
-            return
-
-        self.scion.topology('topology/Tiny.topo', '--sig', '-t', '-n', '242.254.0.0/16')
-        self.set_bs_policies()
-        self.scion.run()
-        if not self.no_docker:
-            self.tools_dc('start', 'tester*')
-            self.docker_status()
-
-    def set_bs_policies(self):
-        for path in local.path('gen') // 'ISD*/AS*/bs*/bs.toml':
-            cfg_dir = path.dirname if self.no_docker else '/share/conf'
-            policy_file = os.path.join(cfg_dir, 'policy.yml')
-            self.scion.set_configs({
-                'bs.policies.Propagation': policy_file,
-                'bs.policies.CoreRegistration': policy_file,
-                'bs.policies.UpRegistration': policy_file,
-                'bs.policies.DownRegistration': policy_file,
-            }, [path])
-            with open(os.path.join(path.dirname, 'policy.yml'), 'w') as outfile:
-                yaml.dump({'MaxExpTime': 0}, outfile, default_flow_style=False)
-
-
-@Test.subcommand("run")
-class TestRun(Base):
-    sig_ping = local['./bin/sig_ping_acceptance']
-
-    @LogExec(logger, "run")
-    def main(self):
-        if not self.enabled:
-            return
-
-        stop = datetime.now() + timedelta(minutes=6)
-        while datetime.now() < stop:
-            args = ['-log.console', 'info']
-            if not self.no_docker:
-                args.append('-d')
-            self.sig_ping(*args)
-            logger.info("Pinging SIGs successful")
-
-
-if __name__ == '__main__':
-    init_log()
-    Test.run()
diff --git a/go/lib/sciond/fake/fake_test.go b/go/lib/sciond/fake/fake_test.go
index fa37d486f..46d05629b 100644
--- a/go/lib/sciond/fake/fake_test.go
+++ b/go/lib/sciond/fake/fake_test.go
@@ -27,7 +27,7 @@ func TestJSONConversion(t *testing.T) {
 	script := &fake.Script{
 		Entries: []*fake.Entry{
 			{
-				Seconds: 0,
+				ReplyStartTimestamp: 0,
 				Paths: []*fake.Path{
 					{
 						JSONFingerprint: "Foo",
@@ -35,7 +35,8 @@ func TestJSONConversion(t *testing.T) {
 							IP:   net.IP{192, 168, 0, 1},
 							Port: 80,
 						},
-						JSONIA: xtest.MustParseIA("1-ff00:0:1"),
+						JSONIA:                  xtest.MustParseIA("1-ff00:0:1"),
+						JSONExpirationTimestamp: 7200,
 					},
 				},
 			},
@@ -65,7 +66,7 @@ func TestPaths(t *testing.T) {
 	script := &fake.Script{
 		Entries: []*fake.Entry{
 			{
-				Seconds: 0,
+				ReplyStartTimestamp: 0,
 				Paths: []*fake.Path{
 					{
 						JSONFingerprint: "Foo",
@@ -73,12 +74,13 @@ func TestPaths(t *testing.T) {
 							IP:   net.IP{10, 0, 0, 1},
 							Port: 80,
 						},
-						JSONIA: xtest.MustParseIA("1-ff00:0:1"),
+						JSONIA:                  xtest.MustParseIA("1-ff00:0:1"),
+						JSONExpirationTimestamp: 7200,
 					},
 				},
 			},
 			{
-				Seconds: 1,
+				ReplyStartTimestamp: 1,
 				Paths: []*fake.Path{
 					{
 						JSONFingerprint: "Foo2",
@@ -86,7 +88,8 @@ func TestPaths(t *testing.T) {
 							IP:   net.IP{10, 0, 0, 2},
 							Port: 80,
 						},
-						JSONIA: xtest.MustParseIA("2-ff00:0:2"),
+						JSONIA:                  xtest.MustParseIA("2-ff00:0:2"),
+						JSONExpirationTimestamp: 10800,
 					},
 				},
 			},
@@ -110,8 +113,9 @@ func TestPaths(t *testing.T) {
 	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
+	// path valid for more than an hour, but less than three
 	assert.True(t, paths[0].Expiry().After(time.Now().Add(time.Hour)))
+	assert.True(t, paths[0].Expiry().Before(time.Now().Add(3*time.Hour)))
 
 	time.Sleep(time.Second)
 
@@ -131,8 +135,9 @@ func TestPaths(t *testing.T) {
 	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)))
+	// path valid for more than two hours, but less than four
+	assert.True(t, paths[0].Expiry().After(time.Now().Add(2*time.Hour)))
+	assert.True(t, paths[0].Expiry().Before(time.Now().Add(4*time.Hour)))
 }
 
 func TestPathCopy(t *testing.T) {
diff --git a/go/lib/sciond/fake/testdata/sd.json b/go/lib/sciond/fake/testdata/sd.json
index 33dc08391..1886f8027 100644
--- a/go/lib/sciond/fake/testdata/sd.json
+++ b/go/lib/sciond/fake/testdata/sd.json
@@ -1,12 +1,13 @@
 {
     "entries": [
       {
-        "seconds": 0,
+        "reply_start_timestamp": 0,
         "paths": [
           {
             "fingerprint": "Foo",
             "next_hop": "192.168.0.1:80",
-            "ia": "1-ff00:0:1"
+            "ia": "1-ff00:0:1",
+            "expiration_timestamp": 7200
           }
         ]
       }