v0.5.0 22211fd

tcprelay_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 4

Tags 11

↑↓ moveenter openesc close