Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
46cc17c
New test using tiny topo.
juagargi Mar 28, 2025
300b45d
Refactor test.
juagargi Mar 28, 2025
c4c1a3a
First phase of multihoming test.
juagargi Mar 28, 2025
04cad6b
Temporary changes.
juagargi Mar 28, 2025
7b2db98
Multihomed behavior through UDP socket creation.
juagargi Sep 3, 2025
1854257
Limit cache size.
juagargi Sep 3, 2025
f5f8e43
Make multihomed tests more robust to concurrent running.
juagargi Sep 3, 2025
cd14a4d
Fix gateway/dataplane TestNoLeak by allowing to stop the continous st…
juagargi Sep 4, 2025
708e599
Do not obtain local address at Conn creation, do it at WriteTo.
juagargi Sep 10, 2025
2a286d4
More efficient & clear mutex locking in OutboundIP.
juagargi Sep 10, 2025
d2b1582
Remove no longer valid comment.
juagargi Sep 10, 2025
8da7d47
Remove not needed double locking of the cache in OutboundIP.
juagargi Sep 10, 2025
90228c0
The ticker object is local to continuousCheckInterfaces.
juagargi Sep 25, 2025
4c31ce3
Add benchmarks measuring sync.Map and RWMutex+map.
juagargi Sep 25, 2025
9e32756
Use slices.Equal instead of custom function.
juagargi Sep 25, 2025
15477a3
Use RUnlock() instead of RLocker().Unlock().
juagargi Sep 25, 2025
58aaa1a
Fix comment.
juagargi Sep 25, 2025
46fd443
Simplify storage of localAddresses to just a slice.
juagargi Sep 25, 2025
0e31eed
Add comment clarifying interactions with NAT+STUN.
juagargi Sep 25, 2025
ce9224b
Fix build after rebasing on last master.
juagargi May 22, 2026
9d5b5d5
Move the unit test to an acceptance test.
juagargi May 22, 2026
3162dd7
Simplify test: server at 110, clients at 111 and 112.
juagargi May 22, 2026
25f1483
Remove deprecated unit test.
juagargi May 22, 2026
1573fb5
Linter findings: long lines and rand.Read in test without err check.
juagargi May 29, 2026
4f59eea
Linter: flake8 new line required.
juagargi May 29, 2026
f987d0c
More robust network attachment to server container.
juagargi May 29, 2026
023787b
Kill previous server processes.
juagargi Jun 1, 2026
f4e4d18
Debug output added.
juagargi Jun 1, 2026
49cb926
Fix: local IP depends on IPv4 vs IPv6.
juagargi Jun 1, 2026
89e50e1
More debug output, server side.
juagargi Jun 1, 2026
a8a756e
Server starts with nohup.
juagargi Jun 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions acceptance/multihomed/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
load("//acceptance/common:topogen.bzl", "topogen_test")

topogen_test(
name = "test",
src = "test.py",
args = [
"--executable=test-client:$(location //acceptance/multihomed/test-client)",
"--executable=test-server:$(location //acceptance/multihomed/test-server)",
],
data = [
"//acceptance/multihomed/test-client",
"//acceptance/multihomed/test-server",
],
topo = "//topology:tiny.topo",
)
19 changes: 19 additions & 0 deletions acceptance/multihomed/test-client/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
load("@rules_go//go:def.bzl", "go_binary", "go_library")

go_library(
name = "go_default_library",
srcs = ["main.go"],
importpath = "github.com/scionproto/scion/acceptance/multihomed/test-client",
visibility = ["//visibility:private"],
deps = [
"//pkg/daemon:go_default_library",
"//pkg/daemon/types:go_default_library",
"//pkg/snet:go_default_library",
],
)

go_binary(
name = "test-client",
embed = [":go_default_library"],
visibility = ["//visibility:public"],
)
144 changes: 144 additions & 0 deletions acceptance/multihomed/test-client/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// Copyright 2026 ETH Zurich
//
// 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.

package main

import (
"context"
"flag"
"fmt"
"log"
"os"

"github.com/scionproto/scion/pkg/daemon"
daemontypes "github.com/scionproto/scion/pkg/daemon/types"
"github.com/scionproto/scion/pkg/snet"
)

func main() {
log.SetOutput(os.Stdout)
log.Printf("test-client starting")

// Parse test inputs. The remote is provided as a full SCION UDP address so the same
// client binary can probe server primary and secondary IPs without code changes.
var daemonAddr string
var localAddr snet.UDPAddr
var remoteAddr snet.UDPAddr
var expect string
var expectAddr *snet.UDPAddr

flag.StringVar(&daemonAddr, "daemon", "", "SCION daemon address")
flag.Var(&localAddr, "local", "Local SCION address")
flag.Var(&remoteAddr, "remote", "Remote SCION address")
flag.StringVar(&expect, "expect", "", "Expected remote SCION address")
flag.Parse()
log.Printf("parsed args daemon=%q local=%q remote=%q expect=%q",
daemonAddr, localAddr.String(), remoteAddr.String(), expect)
log.Printf("local address. IA=%s, IP=%s, port=%d",
localAddr.IA,
localAddr.Host.IP,
localAddr.Host.Port,
)

if expect != "" {
parsed, err := snet.ParseUDPAddr(expect)
if err != nil {
log.Fatalf("parse expected remote address: %v", err)
}
expectAddr = parsed
}

if daemonAddr == "" {
daemonAddr = os.Getenv("SCION_DAEMON_ADDRESS")
}
if daemonAddr == "" {
daemonAddr = os.Getenv("SCION_DAEMON")
}
if daemonAddr == "" {
log.Fatal("daemon address missing: pass -daemon or set SCION_DAEMON_ADDRESS/SCION_DAEMON")
}
log.Printf("using daemon address %s", daemonAddr)

// Resolve a path from local IA to remote IA.
ctx := context.Background()
sd, err := daemon.NewService(daemonAddr).Connect(ctx)
if err != nil {
log.Fatalf("connect daemon: %v", err)
}
defer sd.Close()
log.Printf("connected to daemon")

paths, err := sd.Paths(ctx, remoteAddr.IA, localAddr.IA,
daemontypes.PathReqFlags{Refresh: true})
if err != nil {
log.Fatalf("path lookup: %v", err)
}
if len(paths) == 0 {
log.Fatalf("no path from %s to %s", localAddr.IA, remoteAddr.IA)
}
sp := paths[0]
log.Printf("path lookup returned %d paths; using first path", len(paths))

// Build a SCION connection pinned to the selected path.
topo, err := daemon.LoadTopology(ctx, sd)
if err != nil {
log.Fatalf("load topology: %v", err)
}
remoteAddr.Path = sp.Dataplane()
remoteAddr.NextHop = sp.UnderlayNextHop()

sn := snet.SCIONNetwork{
Topology: topo,
SCMPHandler: snet.DefaultSCMPHandler{
RevocationHandler: daemon.RevHandler{Connector: sd},
},
}

conn, err := sn.Dial(ctx, "udp", localAddr.Host, &remoteAddr)
if err != nil {
log.Fatalf("dial: %v", err)
}
defer conn.Close()
log.Printf("dial successful: local_host=%s remote=%s", localAddr.Host, remoteAddr.String())

// Exchange ping/pong payloads and assert reply endpoint if requested by the caller.
_, err = conn.Write([]byte("ping"))
if err != nil {
log.Fatalf("write ping: %v", err)
}
log.Printf("ping sent")

buf := make([]byte, 2048)
n, from, err := conn.ReadFrom(buf)
if err != nil {
log.Fatalf("read pong: %v", err)
}
log.Printf("received response from %s (%d bytes)", from, n)
if string(buf[:n]) != "pong" {
log.Fatalf("unexpected payload: %q", string(buf[:n]))
}
if expectAddr != nil {
got, ok := from.(*snet.UDPAddr)
if !ok {
log.Fatalf("unexpected remote type %T", from)
}
if got.IA != expectAddr.IA || got.Host.Port != expectAddr.Host.Port ||
!got.Host.IP.Equal(expectAddr.Host.IP) {
log.Fatalf("unexpected remote. got=%s want=%s", got, expectAddr)
}
}

log.Printf("client success remote=%s", from)
fmt.Printf("test-client done\n")
}
15 changes: 15 additions & 0 deletions acceptance/multihomed/test-server/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
load("@rules_go//go:def.bzl", "go_binary", "go_library")

go_library(
name = "go_default_library",
srcs = ["main.go"],
importpath = "github.com/scionproto/scion/acceptance/multihomed/test-server",
visibility = ["//visibility:private"],
deps = ["//pkg/snet:go_default_library"],
)

go_binary(
name = "test-server",
embed = [":go_default_library"],
visibility = ["//visibility:public"],
)
109 changes: 109 additions & 0 deletions acceptance/multihomed/test-server/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright 2026 ETH Zurich
//
// 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.

package main

import (
"flag"
"fmt"
"log"
"net"
"os"
"strconv"

"github.com/scionproto/scion/pkg/snet"
)

func main() {
log.SetOutput(os.Stdout)
log.Printf("test-server starting")

// Parse test inputs. The same server binary is used for:
// - IPv4 unbound mode: bind to 0.0.0.0
// - IPv6 unbound mode: bind to ::
var bindAddr string
var port int

flag.StringVar(&bindAddr, "bind", "0.0.0.0", "Bind host")
flag.IntVar(&port, "port", 31000, "Bind UDP port")
flag.Parse()
log.Printf("parsed args bind=%q port=%d", bindAddr, port)

// Bind a raw UDP socket in the tester namespace. Replies are created by reversing the
// received SCION packet, which preserves the destination address the client originally used.
local, err := net.ResolveUDPAddr("udp", net.JoinHostPort(bindAddr, portString(port)))
if err != nil {
log.Fatalf("parse bind address: %v", err)
}
conn, err := net.ListenUDP("udp", local)
if err != nil {
log.Fatalf("listen: %v", err)
}
defer conn.Close()

log.Printf("server running bind=%s:%d", bindAddr, port)
fmt.Printf("test-server listening\n")

// One-time ping/pong exchange; process exits afterwards,
// so a new server can be started with a fresh bind to the same port.
var pkt snet.Packet
pkt.Prepare()
n, lastHop, err := conn.ReadFrom(pkt.Bytes)
if err != nil {
log.Fatalf("read ping: %v", err)
}
log.Printf("received packet from lastHop=%v bytes=%d", lastHop, n)
pkt.Bytes = pkt.Bytes[:n]

if err := pkt.Decode(); err != nil {
log.Fatalf("decode packet: %v", err)
}
pld, ok := pkt.Payload.(snet.UDPPayload)
if !ok {
log.Fatalf("unexpected payload type %T", pkt.Payload)
}
if string(pld.Payload) != "ping" {
log.Fatalf("unexpected payload: %q", string(pld.Payload))
}

rawPath, ok := pkt.Path.(snet.RawPath)
if !ok {
log.Fatalf("unexpected path type %T", pkt.Path)
}
replyPath, err := snet.DefaultReplyPather{}.ReplyPath(rawPath)
if err != nil {
log.Fatalf("reverse path: %v", err)
}

pkt.Destination, pkt.Source = pkt.Source, pkt.Destination
pkt.Path = replyPath
pkt.Payload = snet.UDPPayload{
SrcPort: pld.DstPort,
DstPort: pld.SrcPort,
Payload: []byte("pong"),
}
if err := pkt.Serialize(); err != nil {
log.Fatalf("serialize reply: %v", err)
}
if _, err := conn.WriteTo(pkt.Bytes, lastHop); err != nil {
log.Fatalf("write pong: %v", err)
}

log.Printf("served ping from %s", pkt.Destination)
fmt.Printf("test-server done\n")
}

func portString(port int) string {
return strconv.Itoa(port)
}
Loading
Loading