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) } }