1// SPDX-License-Identifier: MIT
2
3package tcprelay
4
5import (
6 "bufio"
7 "context"
8 "io"
9 "net"
10 "net/netip"
11 "strconv"
12 "testing"
13 "time"
14)
15
16var lo = netip.MustParseAddr("127.0.0.1")
17
18// echo starts a line echo server and returns its address.
19func echo(t *testing.T) string {
20 t.Helper()
21 ln, err := net.Listen("tcp", "127.0.0.1:0")
22 if err != nil {
23 t.Skip("cannot bind loopback:", err)
24 }
25 t.Cleanup(func() { _ = ln.Close() })
26 go func() {
27 for {
28 c, err := ln.Accept()
29 if err != nil {
30 return
31 }
32 go func() { defer func() { _ = c.Close() }(); _, _ = io.Copy(c, c) }()
33 }
34 }()
35 return ln.Addr().String()
36}
37
38func dialRelay(t *testing.T, r *Relay) net.Conn {
39 t.Helper()
40 a, ok := r.Addr(lo)
41 if !ok {
42 t.Fatal("relay not listening")
43 }
44 c, err := net.DialTimeout("tcp", a.String(), 2*time.Second)
45 if err != nil {
46 t.Fatal(err)
47 }
48 t.Cleanup(func() { _ = c.Close() })
49 return c
50}
51
52func TestRelayForwardsBothWays(t *testing.T) {
53 r := New(echo(t))
54 if err := r.Listen(context.Background(), lo, 0); err != nil {
55 t.Fatal(err)
56 }
57 t.Cleanup(r.CloseAll)
58 c := dialRelay(t, r)
59 _ = c.SetDeadline(time.Now().Add(5 * time.Second))
60 if _, err := c.Write([]byte("hello\n")); err != nil {
61 t.Fatal(err)
62 }
63 line, err := bufio.NewReader(c).ReadString('\n')
64 if err != nil || line != "hello\n" {
65 t.Fatalf("got %q, %v", line, err)
66 }
67}
68
69func TestRelayListenTwiceFails(t *testing.T) {
70 r := New(echo(t))
71 if err := r.Listen(context.Background(), lo, 0); err != nil {
72 t.Fatal(err)
73 }
74 t.Cleanup(r.CloseAll)
75 if err := r.Listen(context.Background(), lo, 0); err == nil {
76 t.Fatal("a second listener on the same address was accepted")
77 }
78}
79
80func TestRelayCloseStopsListenerAndConnections(t *testing.T) {
81 r := New(echo(t))
82 if err := r.Listen(context.Background(), lo, 0); err != nil {
83 t.Fatal(err)
84 }
85 a, _ := r.Addr(lo)
86 c := dialRelay(t, r)
87 if _, err := c.Write([]byte("x\n")); err != nil {
88 t.Fatal(err)
89 }
90 _, _ = bufio.NewReader(c).ReadString('\n')
91 r.Close(lo)
92 if _, ok := r.Addr(lo); ok {
93 t.Fatal("still listening after Close")
94 }
95 _ = c.SetReadDeadline(time.Now().Add(2 * time.Second))
96 if _, err := c.Read(make([]byte, 1)); err == nil {
97 t.Fatal("relayed connection still open after Close")
98 }
99 if c2, err := net.DialTimeout("tcp", a.String(), time.Second); err == nil {
100 _ = c2.Close()
101 t.Fatal("new connections still accepted after Close")
102 }
103 r.Close(lo) // idempotent
104}
105
106// By default Listen fails on an address the host does not own; FreeBind
107// (podman: the bridge address comes when the first container starts) binds it.
108func TestRelayFreeBind(t *testing.T) {
109 ip := netip.MustParseAddr("203.0.113.77") // TEST-NET-3: never configured here
110 r := New("127.0.0.1:1")
111 t.Cleanup(r.CloseAll)
112 if err := r.Listen(context.Background(), ip, 0); err == nil {
113 t.Fatal("listening on an address the host does not own must fail without FreeBind")
114 }
115 r.FreeBind = true
116 if err := r.Listen(context.Background(), ip, 0); err != nil {
117 t.Fatalf("FreeBind: %v", err)
118 }
119 if _, ok := r.Addr(ip); !ok {
120 t.Fatal("no listener")
121 }
122}
123
124// At most MaxConns connections are relayed at once; the next one is closed,
125// and a slot freed by a closed connection is reused.
126func TestRelayConnectionLimit(t *testing.T) {
127 r := New(echo(t))
128 if err := r.Listen(context.Background(), lo, 0); err != nil {
129 t.Fatal(err)
130 }
131 t.Cleanup(r.CloseAll)
132 roundTrip := func(c net.Conn) error {
133 _ = c.SetDeadline(time.Now().Add(5 * time.Second))
134 if _, err := c.Write([]byte("x\n")); err != nil {
135 return err
136 }
137 _, err := bufio.NewReader(c).ReadString('\n')
138 return err
139 }
140 conns := make([]net.Conn, MaxConns)
141 for i := range conns {
142 conns[i] = dialRelay(t, r)
143 if err := roundTrip(conns[i]); err != nil {
144 t.Fatalf("connection %d: %v", i, err)
145 }
146 }
147 extra := dialRelay(t, r)
148 if err := roundTrip(extra); err == nil {
149 t.Fatal("a connection over MaxConns was relayed")
150 }
151 _ = conns[0].Close()
152 deadline := time.Now().Add(5 * time.Second)
153 for {
154 c := dialRelay(t, r)
155 if roundTrip(c) == nil {
156 break
157 }
158 if time.Now().After(deadline) {
159 t.Fatal("the freed slot was not reused")
160 }
161 time.Sleep(20 * time.Millisecond)
162 }
163}
164
165func TestRelayUnreachableTargetClosesTheGuestSide(t *testing.T) {
166 ln, err := net.Listen("tcp", "127.0.0.1:0")
167 if err != nil {
168 t.Skip(err)
169 }
170 port := ln.Addr().(*net.TCPAddr).Port
171 _ = ln.Close() // nothing listens there now
172 r := New(net.JoinHostPort("127.0.0.1", strconv.Itoa(port)))
173 if err := r.Listen(context.Background(), lo, 0); err != nil {
174 t.Fatal(err)
175 }
176 t.Cleanup(r.CloseAll)
177 c := dialRelay(t, r)
178 _ = c.SetReadDeadline(time.Now().Add(5 * time.Second))
179 if _, err := c.Read(make([]byte, 1)); err == nil {
180 t.Fatal("expected the connection to be closed")
181 }
182}