Skip to content

Commit 1b1893e

Browse files
committed
Merge branch 'master' into marcfrei/directfetcher
2 parents 06715fe + 8d42e15 commit 1b1893e

File tree

24 files changed

+1373
-77
lines changed

24 files changed

+1373
-77
lines changed

.golangci.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,13 @@ linters:
9191
- linters:
9292
- goheader
9393
path: pkg/scrypto/cms
94+
- linters:
95+
- goheader
96+
path: pkg/stun/stun.go
97+
- linters:
98+
- goheader
99+
- lll
100+
path: pkg/stun/stun_test.go
94101
paths:
95102
- third_party$
96103
- builtin$

MODULE.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ use_repo(
8989
"com_github_stretchr_testify",
9090
"com_github_uber_jaeger_client_go",
9191
"com_github_vishvananda_netlink",
92+
"com_tailscale",
9293
"in_gopkg_yaml_v3",
9394
"org_go4_netipx",
9495
"org_golang_google_grpc",

acceptance/common/docker.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,21 @@ def execute(self, container, *args, **kwargs):
147147
return self("exec", "-T", "--user", user, container,
148148
"timeout", "1m", *args, **kwargs)
149149

150+
def execute_detached(self, container, *args, **kwargs):
151+
"""Executes an arbitrary command in the specified container.
152+
153+
There's one minute timeout on the command so that tests don't get stuck.
154+
155+
Args:
156+
container: the name of the container to execute the command in.
157+
158+
Returns:
159+
The output of the command.
160+
"""
161+
user = kwargs.get("user", "{}:{}".format(os.getuid(), os.getgid()))
162+
return self("exec", "-d", "-T", "--user", user, container,
163+
"timeout", "1m", *args, **kwargs)
164+
150165
def execute_as_user(self, container, user, *args, **kwargs):
151166
"""Executes an arbitrary command in the specified container.
152167

demo/stun/BUILD.bazel

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
load("//:scion.bzl", "scion_go_binary")
2+
load("//acceptance/common:topogen.bzl", "topogen_test")
3+
4+
topogen_test(
5+
name = "test",
6+
src = "test.py",
7+
args = [
8+
"--executable=test-client:$(location //demo/stun/test-client)",
9+
"--executable=test-server:$(location //demo/stun/test-server)",
10+
],
11+
data = [
12+
"//demo/stun/test-client",
13+
"//demo/stun/test-server",
14+
],
15+
topo = "//topology:tiny.topo",
16+
)

demo/stun/README.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# STUN Demo
2+
3+
This demo shows how a client can use the STUN server implemented at the border router to determine
4+
its public facing IP address and port. This is useful in case the client is behind a NAT.
5+
The client can subsequently use the determined address as its source address in SCION communication,
6+
to ensure returning packets are correctly delivered back to the client.
7+
8+
Note that this demo handles all STUN requests manually to demonstrate how STUN can be implemented in
9+
SCION. Our goal is to integrate these requests in client libraries so that STUN is performed
10+
automatically and transparently for clients.
11+
12+
The topology used in the demo is based on `tiny.topo`.
13+
An additional network was added to simulate a private network inside AS `1-ff00:0:110`.
14+
An additional docker container was added to act as a NAT between the private network and the AS.
15+
The tester container was moved to within the private network.
16+
17+
```text
18+
+-----------------------+
19+
| AS 1-ff00:0:110 |
20+
| +---------------+ |
21+
| | Tester | |
22+
| | (Test-Server) | |
23+
| +---------------+ |
24+
+-----------------------+
25+
| |
26+
| |
27+
--------------+ +--------------
28+
| |
29+
+--------------------------+ +-------------------------+
30+
| AS 1-ff00:0:111 | | AS 1-ff00:0:112 |
31+
| | | |
32+
| | | |
33+
| +--------------------+ | | |
34+
| | Private Subnet | | | |
35+
| | +----------+ | | | |
36+
| | | NAT | | | | |
37+
| | +----------+ | | | |
38+
| | | | | | |
39+
| | +---------------+ | | | |
40+
| | | Tester | | | | |
41+
| | | (Test-Client) | | | | |
42+
| | +---------------+ | | | |
43+
| +--------------------+ | +-------------------------+
44+
+--------------------------+
45+
```
46+
47+
The demo consists of two components: A test client and a test server.
48+
The test client is run within the private network behind the NAT,
49+
and tries to contact the test server, which is located in a different AS.
50+
51+
The demo consists of the following steps:
52+
53+
1. Generate, configure, and start topology
54+
2. Client performs STUN request
55+
3. Client sends SCION packet using determined public address to server
56+
4. Server replies with a SCION packet to client
57+
58+
## Run the Demo
59+
60+
1. [Set up the development environment](https://docs.scion.org/en/latest/build/setup.html)
61+
2. `bazel test --test_output=streamed --cache_test_results=no //demo/stun:test`

demo/stun/test-client/BUILD.bazel

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
load("@rules_go//go:def.bzl", "go_binary", "go_library")
2+
3+
go_library(
4+
name = "go_default_library",
5+
srcs = ["main.go"],
6+
importpath = "github.com/scionproto/scion/demo/stun/test-client",
7+
visibility = ["//visibility:private"],
8+
deps = [
9+
"//pkg/addr:go_default_library",
10+
"//pkg/daemon:go_default_library",
11+
"//pkg/snet:go_default_library",
12+
"@com_tailscale//net/stun:go_default_library",
13+
],
14+
)
15+
16+
go_binary(
17+
name = "test-client",
18+
embed = [":go_default_library"],
19+
visibility = ["//visibility:public"],
20+
)

demo/stun/test-client/main.go

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
// Copyright 2026 ETH Zurich
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package main
16+
17+
import (
18+
"context"
19+
"flag"
20+
"log"
21+
"net"
22+
"net/netip"
23+
"os"
24+
25+
"github.com/scionproto/scion/pkg/addr"
26+
"github.com/scionproto/scion/pkg/daemon"
27+
"github.com/scionproto/scion/pkg/snet"
28+
29+
"tailscale.com/net/stun"
30+
)
31+
32+
// This demo handles all STUN requests manually to demonstrate how STUN can be implemented in SCION.
33+
// Normal clients should use a client library that performs STUN automatically and transparently.
34+
35+
func main() {
36+
log.SetOutput(os.Stdout)
37+
log.Println("Client running")
38+
39+
var daemonAddr string
40+
var localAddr snet.UDPAddr
41+
var remoteAddr snet.UDPAddr
42+
var data string
43+
flag.StringVar(&daemonAddr, "daemon", "127.0.0.1:30255", "Daemon address")
44+
flag.Var(&localAddr, "local", "Local address")
45+
flag.Var(&remoteAddr, "remote", "Remote address")
46+
flag.StringVar(&data, "data", "", "Data")
47+
flag.Parse()
48+
49+
ctx := context.Background()
50+
51+
dc, err := daemon.NewService(daemonAddr).Connect(ctx)
52+
if err != nil {
53+
log.Fatalf("Failed to create SCION daemon connector: %v", err)
54+
}
55+
56+
ps, err := dc.Paths(ctx, remoteAddr.IA, localAddr.IA, daemon.PathReqFlags{Refresh: true})
57+
if err != nil {
58+
log.Fatalf("Failed to lookup paths: %v", err)
59+
}
60+
61+
if len(ps) == 0 {
62+
log.Fatalf("No paths to %v available", remoteAddr.IA)
63+
}
64+
65+
log.Printf("Available paths to %v:", remoteAddr.IA)
66+
for _, p := range ps {
67+
log.Printf("\t%v", p)
68+
}
69+
70+
sp := ps[0]
71+
72+
log.Printf("Selected path to %v:", remoteAddr.IA)
73+
log.Printf("\t%v", sp)
74+
75+
conn, err := net.ListenUDP("udp", localAddr.Host)
76+
if err != nil {
77+
log.Fatalf("Failed to bind UDP connection: %v", err)
78+
}
79+
defer conn.Close()
80+
81+
var srcAddr netip.Addr
82+
var srcPort uint16
83+
var ok bool
84+
85+
nextHop := sp.UnderlayNextHop()
86+
if nextHop == nil && remoteAddr.IA.Equal(localAddr.IA) {
87+
srcAddr, ok = netip.AddrFromSlice(localAddr.Host.IP)
88+
if !ok {
89+
log.Fatalf("Unexpected source address type")
90+
}
91+
92+
// No STUN needed in intra-AS case
93+
srcAddr = srcAddr.Unmap()
94+
srcPort = uint16(localAddr.Host.Port)
95+
nextHop = remoteAddr.Host
96+
} else if nextHop == nil {
97+
log.Fatalf("Unexpected nil next hop for inter-AS path")
98+
} else {
99+
100+
// Generate and send STUN request
101+
txID := stun.NewTxID()
102+
req := stun.Request(txID)
103+
104+
var stunAddr = *nextHop
105+
stunAddr.Port = 30042
106+
107+
_, err = conn.WriteToUDP(req, &stunAddr)
108+
if err != nil {
109+
log.Fatalf("Failed to write STUN packet: %v", err)
110+
}
111+
112+
log.Print("Sent STUN request")
113+
114+
buf := make([]byte, 1024)
115+
n, _, err := conn.ReadFromUDPAddrPort(buf[:])
116+
if err != nil {
117+
log.Fatalf("Failed to read STUN packet: %v", err)
118+
}
119+
120+
// Read STUN response
121+
tid, stunResp, err := stun.ParseResponse(buf[:n])
122+
if err != nil {
123+
log.Fatalf("Failed to decode STUN packet: %v", err)
124+
}
125+
if tid != txID {
126+
log.Fatalf("txid mismatch: got %v, want %v", tid, txID)
127+
}
128+
129+
log.Printf("Received STUN response: %v", stunResp)
130+
131+
// Use address and port from STUN response as source address and port
132+
srcAddr = stunResp.Addr()
133+
srcPort = stunResp.Port()
134+
}
135+
136+
// Continue with normal SCION communication
137+
138+
dstAddr, ok := netip.AddrFromSlice(remoteAddr.Host.IP)
139+
if !ok {
140+
log.Fatal("Unexpected destination address type")
141+
}
142+
dstAddr = dstAddr.Unmap()
143+
144+
pkt := &snet.Packet{
145+
PacketInfo: snet.PacketInfo{
146+
Source: snet.SCIONAddress{
147+
IA: localAddr.IA,
148+
Host: addr.HostIP(srcAddr),
149+
},
150+
Destination: snet.SCIONAddress{
151+
IA: remoteAddr.IA,
152+
Host: addr.HostIP(dstAddr),
153+
},
154+
Path: sp.Dataplane(),
155+
Payload: snet.UDPPayload{
156+
SrcPort: srcPort,
157+
DstPort: uint16(remoteAddr.Host.Port),
158+
Payload: []byte(data),
159+
},
160+
},
161+
}
162+
163+
err = pkt.Serialize()
164+
if err != nil {
165+
log.Fatalf("Failed to serialize SCION packet: %v", err)
166+
}
167+
168+
_, err = conn.WriteTo(pkt.Bytes, nextHop)
169+
if err != nil {
170+
log.Fatalf("Failed to write SCION packet: %v", err)
171+
}
172+
173+
pkt.Prepare()
174+
n, _, err := conn.ReadFrom(pkt.Bytes)
175+
if err != nil {
176+
log.Fatalf("Failed to read SCION packet: %v", err)
177+
}
178+
pkt.Bytes = pkt.Bytes[:n]
179+
180+
err = pkt.Decode()
181+
if err != nil {
182+
log.Fatalf("Failed to decode SCION packet: %v", err)
183+
}
184+
185+
pld, ok := pkt.Payload.(snet.UDPPayload)
186+
if !ok {
187+
log.Fatal("Failed to read packet payload")
188+
}
189+
190+
response := string(pld.Payload)
191+
log.Printf("Received data: \"%s\"", response)
192+
if data != response {
193+
log.Fatalf("Assertion failed: response does not match sent data")
194+
}
195+
}

demo/stun/test-server/BUILD.bazel

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
load("@rules_go//go:def.bzl", "go_binary", "go_library")
2+
3+
go_library(
4+
name = "go_default_library",
5+
srcs = ["main.go"],
6+
importpath = "github.com/scionproto/scion/demo/stun/test-server",
7+
visibility = ["//visibility:private"],
8+
deps = ["//pkg/snet:go_default_library"],
9+
)
10+
11+
go_binary(
12+
name = "test-server",
13+
embed = [":go_default_library"],
14+
visibility = ["//visibility:public"],
15+
)

0 commit comments

Comments
 (0)