Dashboard and API, accounts (organizations, roles, invitations, 2FA, OIDC single sign-on, API keys, revocable sessions), an encrypted SQLite database, resource logins, alerts and /metrics, mail egress management with DKIM keys, and the .env loader. Split out of the original single repository; depends on Reef for the shared types. The end-to-end tests that run Hammerhead, Blacktip and Bamboo together live in e2e/. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
217 lines
6.2 KiB
Go
217 lines
6.2 KiB
Go
package e2e
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.veillax.com/Veillax/blacktip"
|
|
"git.veillax.com/Veillax/reef/model"
|
|
)
|
|
|
|
// freeRun finds n consecutive ports that are free for both TCP and UDP.
|
|
func freeRun(t *testing.T, n int) int {
|
|
t.Helper()
|
|
for try := 0; try < 200; try++ {
|
|
base := 20000 + (time.Now().Nanosecond()/1000+try*37)%30000
|
|
var closers []func()
|
|
ok := true
|
|
for i := 0; i < n && ok; i++ {
|
|
l, err := net.Listen("tcp", "127.0.0.1:"+strconv.Itoa(base+i))
|
|
if err != nil {
|
|
ok = false
|
|
break
|
|
}
|
|
closers = append(closers, func() { l.Close() })
|
|
pc, err := net.ListenPacket("udp", "127.0.0.1:"+strconv.Itoa(base+i))
|
|
if err != nil {
|
|
ok = false
|
|
break
|
|
}
|
|
closers = append(closers, func() { pc.Close() })
|
|
}
|
|
for _, c := range closers {
|
|
c()
|
|
}
|
|
if ok {
|
|
return base
|
|
}
|
|
}
|
|
t.Fatal("no free port block")
|
|
return 0
|
|
}
|
|
|
|
func tcpOn(t *testing.T, port int) {
|
|
l, err := net.Listen("tcp", "127.0.0.1:"+strconv.Itoa(port))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { l.Close() })
|
|
go func() {
|
|
for {
|
|
c, err := l.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
go func() {
|
|
defer c.Close()
|
|
buf := make([]byte, 64)
|
|
n, _ := c.Read(buf)
|
|
fmt.Fprintf(c, "tcp%d:%s", port, buf[:n])
|
|
}()
|
|
}
|
|
}()
|
|
}
|
|
|
|
func udpOn(t *testing.T, port int) {
|
|
pc, err := net.ListenPacket("udp", "127.0.0.1:"+strconv.Itoa(port))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { pc.Close() })
|
|
go func() {
|
|
buf := make([]byte, 64)
|
|
for {
|
|
n, from, err := pc.ReadFrom(buf)
|
|
if err != nil {
|
|
return
|
|
}
|
|
pc.WriteTo([]byte(fmt.Sprintf("udp%d:%s", port, buf[:n])), from)
|
|
}
|
|
}()
|
|
}
|
|
|
|
func TestPortRanges(t *testing.T) {
|
|
w := setup(t, blacktip.TraefikConfig{})
|
|
target := freeRun(t, 3)
|
|
pub := freeRun(t, 3)
|
|
if target == pub {
|
|
pub = freeRun(t, 4) + 0
|
|
}
|
|
for i := 0; i < 3; i++ {
|
|
tcpOn(t, target+i)
|
|
udpOn(t, target+i)
|
|
}
|
|
base := "/api/orgs/" + w.org + "/mappings"
|
|
open := func(proto, ip string, site string, port, end, tport int) (model.Mapping, int, string) {
|
|
var m model.Mapping
|
|
code, raw := w.admin.do("POST", base, map[string]any{"proto": proto, "edgeId": w.edge, "bindIp": ip, "port": port, "portEnd": end,
|
|
"targets": tgt(site, tport), "enabled": true}, &m)
|
|
return m, code, raw
|
|
}
|
|
|
|
tcp, code, raw := open("tcp", "127.0.0.1", w.site, pub, pub+2, target)
|
|
if code != 200 {
|
|
t.Fatalf("open range: %d %s", code, raw)
|
|
}
|
|
if _, code, raw = open("udp", "127.0.0.1", w.site, pub, pub+2, target); code != 200 {
|
|
t.Fatalf("a udp range over the same numbers is a different protocol and must be allowed: %d %s", code, raw)
|
|
}
|
|
if _, code, raw = open("tcp", "127.0.0.2", w.local, pub, pub+2, target); code != 200 {
|
|
t.Fatalf("local range: %d %s", code, raw)
|
|
}
|
|
if tcp.PortEnd != pub+2 {
|
|
t.Errorf("portEnd = %d, want %d", tcp.PortEnd, pub+2)
|
|
}
|
|
|
|
// each public port reaches the target port at the same offset, through the tunnel and directly
|
|
for _, ip := range []string{"127.0.0.1", "127.0.0.2"} {
|
|
for i := 0; i < 3; i++ {
|
|
got := tcpAsk(t, ip+":"+strconv.Itoa(pub+i), "hi")
|
|
if want := fmt.Sprintf("tcp%d:hi", target+i); got != want {
|
|
t.Errorf("%s port %d -> %q, want %q", ip, pub+i, got, want)
|
|
}
|
|
}
|
|
}
|
|
for i := 0; i < 3; i++ {
|
|
got := udpAsk(t, "127.0.0.1:"+strconv.Itoa(pub+i), "yo")
|
|
if want := fmt.Sprintf("udp%d:yo", target+i); got != want {
|
|
t.Errorf("udp port %d -> %q, want %q", pub+i, got, want)
|
|
}
|
|
}
|
|
|
|
// shrinking the range closes the ports that fell off the end
|
|
if code, raw := w.admin.do("PUT", base+"/"+tcp.ID, map[string]any{"proto": "tcp", "edgeId": w.edge, "bindIp": "127.0.0.1", "port": pub, "portEnd": pub + 1,
|
|
"targets": tgt(w.site, target), "enabled": true}, nil); code != 200 {
|
|
t.Fatalf("shrink: %d %s", code, raw)
|
|
}
|
|
deadline := time.Now().Add(6 * time.Second)
|
|
for {
|
|
c, err := net.DialTimeout("tcp", "127.0.0.1:"+strconv.Itoa(pub+2), 500*time.Millisecond)
|
|
if err != nil {
|
|
break
|
|
}
|
|
c.Close()
|
|
if time.Now().After(deadline) {
|
|
t.Fatal("a port past the end of the range stayed open")
|
|
}
|
|
time.Sleep(200 * time.Millisecond)
|
|
}
|
|
if got := tcpAsk(t, "127.0.0.1:"+strconv.Itoa(pub+1), "x"); got != fmt.Sprintf("tcp%d:x", target+1) {
|
|
t.Errorf("a port still inside the range: %q", got)
|
|
}
|
|
|
|
// validation
|
|
bad := func(what, proto string, port, end, tport int) {
|
|
t.Helper()
|
|
if _, code, raw := open(proto, "127.0.0.9", w.site, port, end, tport); code != 400 {
|
|
t.Errorf("%s was accepted (%d %s)", what, code, raw)
|
|
}
|
|
}
|
|
bad("a range on http", "http", 8080, 8082, 80)
|
|
bad("an end before the start", "tcp", 5000, 4990, 80)
|
|
bad("a range of 1001 ports", "tcp", 10000, 11000, 80)
|
|
bad("a range past 65535", "tcp", 65530, 65540, 80)
|
|
bad("targets that would run past 65535", "tcp", 5000, 5009, 65530)
|
|
if _, code, _ := open("tcp", "127.0.0.1", w.site, pub+1, pub+1, target); code != 400 {
|
|
t.Error("a single port inside an existing range must conflict")
|
|
}
|
|
if _, code, _ := open("tcp", "127.0.0.1", w.site, pub-1, pub, target); code != 400 {
|
|
t.Error("a range overlapping the start of an existing range must conflict")
|
|
}
|
|
if _, code, raw := open("tcp", "127.0.0.1", w.site, pub+2, pub+4, target); code != 200 {
|
|
t.Errorf("a range that starts right after the existing one is fine: %d %s", code, raw)
|
|
}
|
|
|
|
// one busy port inside a range is reported, and the others still work
|
|
busyBase := freeRun(t, 3)
|
|
busy, err := net.Listen("tcp", "127.0.0.3:"+strconv.Itoa(busyBase+1))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer busy.Close()
|
|
bm, code, raw := open("tcp", "127.0.0.3", w.local, busyBase, busyBase+2, target)
|
|
if code != 200 {
|
|
t.Fatalf("open busy range: %d %s", code, raw)
|
|
}
|
|
if got := tcpAsk(t, "127.0.0.3:"+strconv.Itoa(busyBase), "a"); got != fmt.Sprintf("tcp%d:a", target) {
|
|
t.Errorf("a free port next to a busy one: %q", got)
|
|
}
|
|
deadline = time.Now().Add(8 * time.Second)
|
|
for {
|
|
var st struct {
|
|
Mappings []struct {
|
|
ID string
|
|
Status model.MappingStatus
|
|
}
|
|
}
|
|
w.admin.must("GET", "/api/orgs/"+w.org+"/state", nil, &st)
|
|
msg := ""
|
|
for _, m := range st.Mappings {
|
|
if m.ID == bm.ID {
|
|
msg = m.Status.Error
|
|
}
|
|
}
|
|
if strings.Contains(msg, strconv.Itoa(busyBase+1)) {
|
|
break
|
|
}
|
|
if time.Now().After(deadline) {
|
|
t.Fatalf("the busy port isn't named in the status: %q", msg)
|
|
}
|
|
time.Sleep(250 * time.Millisecond)
|
|
}
|
|
}
|