Explorar el Código

make kerberos listener optional

Mike hace 11 meses
padre
commit
d635fdab07
Se han modificado 5 ficheros con 449 adiciones y 41 borrados
  1. 0 1
      cmd/server/factory.go
  2. 27 3
      config/config.go
  3. 261 0
      config/config_test.go
  4. 5 0
      server/kerberos/kerberos.go
  5. 156 37
      server/kerberos/kerberos_test.go

+ 0 - 1
cmd/server/factory.go

@@ -96,7 +96,6 @@ func validateConfigMigration() error {
 		"API_LISTENER",
 		"OSCAR_ADVERTISED_LISTENERS",
 		"OSCAR_LISTENERS",
-		"KERBEROS_LISTENERS",
 		"TOC_LISTENERS",
 	}
 

+ 27 - 3
config/config.go

@@ -12,7 +12,7 @@ import (
 type Config struct {
 	BOSListeners       string `envconfig:"OSCAR_LISTENERS" required:"true" val:"LOCAL://0.0.0.0:5190" description:"Network listeners for core OSCAR services. For multi-homed servers, allows users to connect from multiple networks. For example, you can allow both LAN and Internet clients to connect to the same server using different connection settings.\n\nFormat:\n\t- Comma-separated list of [NAME]://[HOSTNAME]:[PORT]\n\t- Listener names and ports must be unique\n\t- Listener names are user-defined\n\t- Each listener needs OSCAR_ADVERTISED_LISTENERS/KERBEROS_LISTENERS configs\n\nExamples:\n\t// Listen on all interfaces\n\tLAN://0.0.0.0:5190\n\t// Separate Internet and LAN config\n\tWAN://142.250.176.206:5190,LAN://192.168.1.10:5191"`
 	BOSAdvertisedHosts string `envconfig:"OSCAR_ADVERTISED_LISTENERS" required:"true" val:"LOCAL://127.0.0.1:5190" description:"Hostnames published by the server that clients connect to for accessing various OSCAR services. These hostnames are NOT the bind addresses. For multi-homed use servers, allows clients to connect using separate hostnames per network.\n\nFormat:\n\t- Comma-separated list of [NAME]://[HOSTNAME]:[PORT]\n\t- Each listener config must correspond to a config in OSCAR_LISTENERS\n\t- Clients MUST be able to connect to these hostnames\n\nExamples:\n\t// Local LAN config, server behind NAT\n\tLAN://0.0.0.0:5190\n\t// Separate Internet and LAN config\n\tWAN://aim.example.com:5190,LAN://192.168.1.10:5191"`
-	KerberosListeners  string `envconfig:"KERBEROS_LISTENERS" required:"true" val:"LOCAL://0.0.0.0:1088" description:"Network listeners for Kerberos authentication. See OSCAR_LISTENERS doc for more details.\n\nExamples:\n\t// Listen on all interfaces\n\tLAN://0.0.0.0:1088\n\t// Separate Internet and LAN config\n\tWAN://142.250.176.206:1088,LAN://192.168.1.10:1087"`
+	KerberosListeners  string `envconfig:"KERBEROS_LISTENERS" required:"false" val:"LOCAL://0.0.0.0:1088" description:"Network listeners for Kerberos authentication. See OSCAR_LISTENERS doc for more details.\n\nExamples:\n\t// Listen on all interfaces\n\tLAN://0.0.0.0:1088\n\t// Separate Internet and LAN config\n\tWAN://142.250.176.206:1088,LAN://192.168.1.10:1087"`
 	TOCListeners       string `envconfig:"TOC_LISTENERS" required:"true" val:"0.0.0.0:9898" description:"Network listeners for TOC protocol service.\n\nFormat: Comma-separated list of hostname:port pairs.\n\nExamples:\n\t// All interfaces\n\t0.0.0.0:9898\n\t// Multiple listeners\n\t0.0.0.0:9898,192.168.1.10:9899"`
 	APIListener        string `envconfig:"API_LISTENER" required:"true" val:"127.0.0.1:8080" description:"Network listener for management API binds to. Only 1 listener can be specified. (Default 127.0.0.1 restricts to same machine only)."`
 
@@ -37,10 +37,17 @@ func ParseListenersCfg(BOSListeners string, BOSAdvertisedListeners string, kerbe
 	m := make(map[string]*Listener)
 
 	for _, lStr := range strings.Split(BOSListeners, ",") {
+		lStr = strings.TrimSpace(lStr)
+		if lStr == "" {
+			continue
+		}
 		u, err := url.Parse(lStr)
 		if err != nil {
 			return nil, fmt.Errorf("parsing listener URI: %w", err)
 		}
+		if u.Scheme == "" {
+			return nil, fmt.Errorf("invalid listener URI: missing scheme in %q", lStr)
+		}
 		if _, ok := m[u.Scheme]; !ok {
 			m[u.Scheme] = &Listener{}
 		}
@@ -51,10 +58,17 @@ func ParseListenersCfg(BOSListeners string, BOSAdvertisedListeners string, kerbe
 	}
 
 	for _, lStr := range strings.Split(BOSAdvertisedListeners, ",") {
+		lStr = strings.TrimSpace(lStr)
+		if lStr == "" {
+			continue
+		}
 		u, err := url.Parse(lStr)
 		if err != nil {
 			return nil, fmt.Errorf("parsing listener URI: %w", err)
 		}
+		if u.Scheme == "" {
+			return nil, fmt.Errorf("invalid listener URI: missing scheme in %q", lStr)
+		}
 		if _, ok := m[u.Scheme]; !ok {
 			m[u.Scheme] = &Listener{}
 		}
@@ -65,10 +79,17 @@ func ParseListenersCfg(BOSListeners string, BOSAdvertisedListeners string, kerbe
 	}
 
 	for _, lStr := range strings.Split(kerberosListeners, ",") {
+		lStr = strings.TrimSpace(lStr)
+		if lStr == "" {
+			continue
+		}
 		u, err := url.Parse(lStr)
 		if err != nil {
 			return nil, fmt.Errorf("parsing listener URI: %w", err)
 		}
+		if u.Scheme == "" {
+			return nil, fmt.Errorf("invalid listener URI: missing scheme in %q", lStr)
+		}
 		if _, ok := m[u.Scheme]; !ok {
 			m[u.Scheme] = &Listener{}
 		}
@@ -86,11 +107,14 @@ func ParseListenersCfg(BOSListeners string, BOSAdvertisedListeners string, kerbe
 			return nil, fmt.Errorf("missing BOS advertise address for listener `%s://`", k)
 		case v.BOSListenAddress == "":
 			return nil, fmt.Errorf("missing BOS listen address for listener `%s://`", k)
-		case v.KerberosListenAddress == "":
-			return nil, fmt.Errorf("missing kerberos listen address for listener `%s://`", k)
 		}
 		ret = append(ret, *v)
 	}
 
+	// Validate that there is at least one BOS listener
+	if len(ret) == 0 {
+		return nil, errors.New("at least one BOS listener is required")
+	}
+
 	return ret, nil
 }

+ 261 - 0
config/config_test.go

@@ -0,0 +1,261 @@
+package config
+
+import (
+	"testing"
+)
+
+func TestParseListenersCfg(t *testing.T) {
+	tests := []struct {
+		name                   string
+		bosListeners           string
+		bosAdvertisedListeners string
+		kerberosListeners      string
+		want                   []Listener
+		wantErr                bool
+		errContains            string
+	}{
+		{
+			name:                   "valid single listener with kerberos",
+			bosListeners:           "LOCAL://0.0.0.0:5190",
+			bosAdvertisedListeners: "LOCAL://127.0.0.1:5190",
+			kerberosListeners:      "LOCAL://0.0.0.0:1088",
+			want: []Listener{
+				{
+					BOSListenAddress:      "0.0.0.0:5190",
+					BOSAdvertisedHost:     "127.0.0.1:5190",
+					KerberosListenAddress: "0.0.0.0:1088",
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name:                   "valid single listener without kerberos",
+			bosListeners:           "LOCAL://0.0.0.0:5190",
+			bosAdvertisedListeners: "LOCAL://127.0.0.1:5190",
+			kerberosListeners:      "",
+			want: []Listener{
+				{
+					BOSListenAddress:      "0.0.0.0:5190",
+					BOSAdvertisedHost:     "127.0.0.1:5190",
+					KerberosListenAddress: "",
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name:                   "valid multiple listeners with mixed kerberos",
+			bosListeners:           "LAN://192.168.1.10:5190,WAN://0.0.0.0:5191",
+			bosAdvertisedListeners: "LAN://192.168.1.10:5190,WAN://example.com:5191",
+			kerberosListeners:      "LAN://192.168.1.10:1088",
+			want: []Listener{
+				{
+					BOSListenAddress:      "192.168.1.10:5190",
+					BOSAdvertisedHost:     "192.168.1.10:5190",
+					KerberosListenAddress: "192.168.1.10:1088",
+				},
+				{
+					BOSListenAddress:      "0.0.0.0:5191",
+					BOSAdvertisedHost:     "example.com:5191",
+					KerberosListenAddress: "",
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name:                   "missing BOS advertised host",
+			bosListeners:           "LOCAL://0.0.0.0:5190",
+			bosAdvertisedListeners: "",
+			kerberosListeners:      "",
+			want:                   nil,
+			wantErr:                true,
+			errContains:            "missing BOS advertise address",
+		},
+		{
+			name:                   "missing BOS listen address",
+			bosListeners:           "",
+			bosAdvertisedListeners: "LOCAL://127.0.0.1:5190",
+			kerberosListeners:      "",
+			want:                   nil,
+			wantErr:                true,
+			errContains:            "missing BOS listen address for listener `local://`",
+		},
+		{
+			name:                   "duplicate listener definition in BOS",
+			bosListeners:           "LOCAL://0.0.0.0:5190,LOCAL://0.0.0.0:5191",
+			bosAdvertisedListeners: "LOCAL://127.0.0.1:5190",
+			kerberosListeners:      "",
+			want:                   nil,
+			wantErr:                true,
+			errContains:            "duplicate listener definition",
+		},
+		{
+			name:                   "duplicate listener definition in advertised",
+			bosListeners:           "LOCAL://0.0.0.0:5190",
+			bosAdvertisedListeners: "LOCAL://127.0.0.1:5190,LOCAL://127.0.0.1:5191",
+			kerberosListeners:      "",
+			want:                   nil,
+			wantErr:                true,
+			errContains:            "duplicate listener definition",
+		},
+		{
+			name:                   "duplicate listener definition in kerberos",
+			bosListeners:           "LOCAL://0.0.0.0:5190",
+			bosAdvertisedListeners: "LOCAL://127.0.0.1:5190",
+			kerberosListeners:      "LOCAL://0.0.0.0:1088,LOCAL://0.0.0.0:1089",
+			want:                   nil,
+			wantErr:                true,
+			errContains:            "duplicate listener definition",
+		},
+		{
+			name:                   "invalid URI format in BOS",
+			bosListeners:           "invalid-uri",
+			bosAdvertisedListeners: "LOCAL://127.0.0.1:5190",
+			kerberosListeners:      "",
+			want:                   nil,
+			wantErr:                true,
+			errContains:            "invalid listener URI: missing scheme",
+		},
+		{
+			name:                   "invalid URI format in advertised",
+			bosListeners:           "LOCAL://0.0.0.0:5190",
+			bosAdvertisedListeners: "invalid-uri",
+			kerberosListeners:      "",
+			want:                   nil,
+			wantErr:                true,
+			errContains:            "invalid listener URI: missing scheme",
+		},
+		{
+			name:                   "invalid URI format in kerberos",
+			bosListeners:           "LOCAL://0.0.0.0:5190",
+			bosAdvertisedListeners: "LOCAL://127.0.0.1:5190",
+			kerberosListeners:      "invalid-uri",
+			want:                   nil,
+			wantErr:                true,
+			errContains:            "invalid listener URI: missing scheme",
+		},
+		{
+			name:                   "complex multi-listener setup",
+			bosListeners:           "LAN://192.168.1.10:5190,WAN://0.0.0.0:5191,DOCKER://172.17.0.1:5192",
+			bosAdvertisedListeners: "DOCKER://172.17.0.1:5192,LAN://192.168.1.10:5190,WAN://example.com:5191",
+			kerberosListeners:      "WAN://0.0.0.0:1089,LAN://192.168.1.10:1088",
+			want: []Listener{
+				{
+					BOSListenAddress:      "192.168.1.10:5190",
+					BOSAdvertisedHost:     "192.168.1.10:5190",
+					KerberosListenAddress: "192.168.1.10:1088",
+				},
+				{
+					BOSListenAddress:      "0.0.0.0:5191",
+					BOSAdvertisedHost:     "example.com:5191",
+					KerberosListenAddress: "0.0.0.0:1089",
+				},
+				{
+					BOSListenAddress:      "172.17.0.1:5192",
+					BOSAdvertisedHost:     "172.17.0.1:5192",
+					KerberosListenAddress: "",
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name:                   "empty strings for all inputs",
+			bosListeners:           "",
+			bosAdvertisedListeners: "",
+			kerberosListeners:      "",
+			want:                   nil,
+			wantErr:                true,
+			errContains:            "at least one BOS listener is required",
+		},
+		{
+			name:                   "whitespace-only strings",
+			bosListeners:           "   ",
+			bosAdvertisedListeners: "   ",
+			kerberosListeners:      "   ",
+			want:                   nil,
+			wantErr:                true,
+			errContains:            "at least one BOS listener is required",
+		},
+		{
+			name:                   "only kerberos listeners provided",
+			bosListeners:           "",
+			bosAdvertisedListeners: "",
+			kerberosListeners:      "LOCAL://0.0.0.0:1088",
+			want:                   nil,
+			wantErr:                true,
+			errContains:            "missing BOS advertise address for listener `local://`",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := ParseListenersCfg(tt.bosListeners, tt.bosAdvertisedListeners, tt.kerberosListeners)
+
+			if tt.wantErr {
+				if err == nil {
+					t.Errorf("ParseListenersCfg() expected error but got none")
+					return
+				}
+				if tt.errContains != "" && !contains(err.Error(), tt.errContains) {
+					t.Errorf("ParseListenersCfg() error = %v, want error containing %q", err, tt.errContains)
+				}
+				return
+			}
+
+			if err != nil {
+				t.Errorf("ParseListenersCfg() unexpected error = %v", err)
+				return
+			}
+
+			if len(got) != len(tt.want) {
+				t.Errorf("ParseListenersCfg() returned %d listeners, want %d", len(got), len(tt.want))
+				return
+			}
+
+			// Create maps for easier comparison
+			gotMap := make(map[string]Listener)
+			wantMap := make(map[string]Listener)
+
+			for _, l := range got {
+				key := l.BOSListenAddress + "|" + l.BOSAdvertisedHost
+				gotMap[key] = l
+			}
+
+			for _, l := range tt.want {
+				key := l.BOSListenAddress + "|" + l.BOSAdvertisedHost
+				wantMap[key] = l
+			}
+
+			for key, wantListener := range wantMap {
+				gotListener, exists := gotMap[key]
+				if !exists {
+					t.Errorf("ParseListenersCfg() missing listener with key %s", key)
+					continue
+				}
+
+				if gotListener.BOSListenAddress != wantListener.BOSListenAddress {
+					t.Errorf("ParseListenersCfg() BOSListenAddress = %v, want %v", gotListener.BOSListenAddress, wantListener.BOSListenAddress)
+				}
+				if gotListener.BOSAdvertisedHost != wantListener.BOSAdvertisedHost {
+					t.Errorf("ParseListenersCfg() BOSAdvertisedHost = %v, want %v", gotListener.BOSAdvertisedHost, wantListener.BOSAdvertisedHost)
+				}
+				if gotListener.KerberosListenAddress != wantListener.KerberosListenAddress {
+					t.Errorf("ParseListenersCfg() KerberosListenAddress = %v, want %v", gotListener.KerberosListenAddress, wantListener.KerberosListenAddress)
+				}
+			}
+		})
+	}
+}
+
+// Helper function to check if a string contains a substring
+func contains(s, substr string) bool {
+	return len(s) >= len(substr) && (s == substr || len(substr) == 0 ||
+		(len(s) > len(substr) && (s[:len(substr)] == substr || s[len(s)-len(substr):] == substr ||
+			func() bool {
+				for i := 1; i <= len(s)-len(substr); i++ {
+					if s[i:i+len(substr)] == substr {
+						return true
+					}
+				}
+				return false
+			}())))
+}

+ 5 - 0
server/kerberos/kerberos.go

@@ -53,6 +53,11 @@ type Server struct {
 }
 
 func (s *Server) ListenAndServe() error {
+	if len(s.servers) == 0 {
+		s.logger.Info("no kerberos listeners defined, moving on")
+		return nil
+	}
+
 	ctx, cancel := context.WithCancel(context.Background())
 	defer cancel()
 

+ 156 - 37
server/kerberos/kerberos_test.go

@@ -6,10 +6,11 @@ import (
 	"fmt"
 	"io"
 	"log/slog"
+	"net"
 	"net/http"
-	"net/http/httptest"
 	"sync"
 	"testing"
+	"time"
 
 	"github.com/mk6i/retro-aim-server/config"
 	"github.com/mk6i/retro-aim-server/wire"
@@ -21,6 +22,7 @@ import (
 func TestKerberosLoginHandler(t *testing.T) {
 	tests := []struct {
 		name               string
+		listeners          []config.Listener
 		request            wire.SNACMessage
 		response           wire.SNACMessage
 		responseErr        error
@@ -29,7 +31,47 @@ func TestKerberosLoginHandler(t *testing.T) {
 		wantStatus         int
 	}{
 		{
-			name: "successful login",
+			name: "successful login with single listener",
+			listeners: []config.Listener{
+				{
+					KerberosListenAddress: ":1088",
+					BOSAdvertisedHost:     "localhost:5190",
+				},
+			},
+			request: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.Kerberos,
+					SubGroup:  wire.KerberosLoginRequest,
+				},
+				Body: wire.SNAC_0x050C_0x0002_KerberosLoginRequest{
+					RequestID: 4321,
+				},
+			},
+			response: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.Kerberos,
+					SubGroup:  wire.KerberosLoginSuccessResponse,
+				},
+				Body: wire.SNAC_0x050C_0x0003_KerberosLoginSuccessResponse{
+					RequestID: 4321,
+				},
+			},
+			expectLogin:        true,
+			expectSNACResponse: true,
+			wantStatus:         http.StatusOK,
+		},
+		{
+			name: "successful login with multiple listeners",
+			listeners: []config.Listener{
+				{
+					KerberosListenAddress: ":1088",
+					BOSAdvertisedHost:     "localhost:5190",
+				},
+				{
+					KerberosListenAddress: ":1089",
+					BOSAdvertisedHost:     "localhost:5191",
+				},
+			},
 			request: wire.SNACMessage{
 				Frame: wire.SNACFrame{
 					FoodGroup: wire.Kerberos,
@@ -52,8 +94,62 @@ func TestKerberosLoginHandler(t *testing.T) {
 			expectSNACResponse: true,
 			wantStatus:         http.StatusOK,
 		},
+		{
+			name: "successful login with three listeners",
+			listeners: []config.Listener{
+				{
+					KerberosListenAddress: ":1088",
+					BOSAdvertisedHost:     "localhost:5190",
+				},
+				{
+					KerberosListenAddress: ":1089",
+					BOSAdvertisedHost:     "localhost:5191",
+				},
+				{
+					KerberosListenAddress: ":1090",
+					BOSAdvertisedHost:     "localhost:5192",
+				},
+			},
+			request: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.Kerberos,
+					SubGroup:  wire.KerberosLoginRequest,
+				},
+				Body: wire.SNAC_0x050C_0x0002_KerberosLoginRequest{
+					RequestID: 4321,
+				},
+			},
+			response: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.Kerberos,
+					SubGroup:  wire.KerberosLoginSuccessResponse,
+				},
+				Body: wire.SNAC_0x050C_0x0003_KerberosLoginSuccessResponse{
+					RequestID: 4321,
+				},
+			},
+			expectLogin:        true,
+			expectSNACResponse: true,
+			wantStatus:         http.StatusOK,
+		},
+		{
+			name:               "no listeners defined - server exits cleanly",
+			listeners:          []config.Listener{},
+			request:            wire.SNACMessage{},
+			response:           wire.SNACMessage{},
+			responseErr:        nil,
+			expectLogin:        false,
+			expectSNACResponse: false,
+			wantStatus:         0, // No server to test against
+		},
 		{
 			name: "invalid request SNAC type",
+			listeners: []config.Listener{
+				{
+					KerberosListenAddress: ":1088",
+					BOSAdvertisedHost:     "localhost:5190",
+				},
+			},
 			request: wire.SNACMessage{
 				Frame: wire.SNACFrame{
 					FoodGroup: wire.ICBM,
@@ -69,6 +165,12 @@ func TestKerberosLoginHandler(t *testing.T) {
 		},
 		{
 			name: "login runtime error",
+			listeners: []config.Listener{
+				{
+					KerberosListenAddress: ":1088",
+					BOSAdvertisedHost:     "localhost:5190",
+				},
+			},
 			request: wire.SNACMessage{
 				Frame: wire.SNACFrame{
 					FoodGroup: wire.Kerberos,
@@ -88,23 +190,22 @@ func TestKerberosLoginHandler(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			mockAuth := newMockAuthService(t)
-			if tt.expectLogin {
-				mockAuth.EXPECT().
-					KerberosLogin(mock.Anything, tt.request.Body, mock.Anything, "localhost:5190").
-					Return(tt.response, tt.responseErr)
-			}
+			log := slog.New(slog.NewTextHandler(io.Discard, nil))
 
-			listenCfg := []config.Listener{
-				{
-					KerberosListenAddress: ":1088",
-					BOSAdvertisedHost:     "localhost:5190",
-				},
+			var srv *Server
+			if len(tt.listeners) > 0 {
+				mockAuth := newMockAuthService(t)
+				if tt.expectLogin {
+					mockAuth.EXPECT().
+						KerberosLogin(mock.Anything, tt.request.Body, mock.Anything, mock.Anything).
+						Return(tt.response, tt.responseErr)
+				}
+				srv = NewKerberosServer(tt.listeners, log, mockAuth)
+			} else {
+				// For no listeners case, we don't need auth service or request data
+				srv = NewKerberosServer(tt.listeners, log, nil)
 			}
 
-			log := slog.New(slog.NewTextHandler(io.Discard, nil))
-			srv := NewKerberosServer(listenCfg, log, mockAuth)
-
 			wg := sync.WaitGroup{}
 			wg.Add(1)
 			go func() {
@@ -112,34 +213,52 @@ func TestKerberosLoginHandler(t *testing.T) {
 				assert.NoError(t, srv.ListenAndServe())
 			}()
 
-			b := &bytes.Buffer{}
-			assert.NoError(t, wire.MarshalBE(tt.request, b))
+			// Wait for server to be ready by checking if ports are listening
+			for i := 0; i < len(tt.listeners); i++ {
+				maxRetries := 10
+				backoff := 5 * time.Millisecond
 
-			req := httptest.NewRequest(http.MethodPost, "/", b)
-			req.Header.Set("Content-Type", "application/x-snac")
+				for attempt := 0; attempt < maxRetries; attempt++ {
+					conn, err := net.Dial("tcp", "localhost"+tt.listeners[i].KerberosListenAddress)
+					if err == nil {
+						conn.Close()
+						break
+					}
+					if attempt == maxRetries-1 {
+						t.Fatalf("Server not ready after %d attempts: %v", maxRetries, err)
+					}
+					time.Sleep(backoff)
+					backoff *= 2
+				}
+			}
 
-			resp, err := http.Post(fmt.Sprintf("http://localhost:%s", "1088"), "application/x-snac", b)
-			assert.NoError(t, err)
-			defer resp.Body.Close()
+			// Test against all listeners
+			for i, listener := range tt.listeners {
+				b := &bytes.Buffer{}
+				assert.NoError(t, wire.MarshalBE(tt.request, b))
 
-			assert.Equal(t, tt.wantStatus, resp.StatusCode)
+				resp, err := http.Post(fmt.Sprintf("http://localhost:%s", listener.KerberosListenAddress[1:]), "application/x-snac", b)
+				assert.NoError(t, err)
+				defer resp.Body.Close()
 
-			if tt.expectSNACResponse {
-				respBytes, _ := io.ReadAll(resp.Body)
-				reader := bytes.NewReader(respBytes)
-				haveFrame := wire.SNACFrame{}
-				assert.NoError(t, wire.UnmarshalBE(&haveFrame, reader))
-				assert.Equal(t, tt.response.Frame, haveFrame)
-				haveBody := wire.SNAC_0x050C_0x0003_KerberosLoginSuccessResponse{}
-				assert.NoError(t, wire.UnmarshalBE(&haveBody, reader))
-				assert.Equal(t, tt.response.Body, haveBody)
-				assert.Equal(t, "application/x-snac", resp.Header.Get("Content-Type"))
-			} else {
-				assert.Equal(t, "text/plain; charset=utf-8", resp.Header.Get("Content-Type"))
+				assert.Equal(t, tt.wantStatus, resp.StatusCode, "listener %d at %s", i, listener.KerberosListenAddress)
+
+				if tt.expectSNACResponse {
+					respBytes, _ := io.ReadAll(resp.Body)
+					reader := bytes.NewReader(respBytes)
+					haveFrame := wire.SNACFrame{}
+					assert.NoError(t, wire.UnmarshalBE(&haveFrame, reader))
+					assert.Equal(t, tt.response.Frame, haveFrame)
+					haveBody := wire.SNAC_0x050C_0x0003_KerberosLoginSuccessResponse{}
+					assert.NoError(t, wire.UnmarshalBE(&haveBody, reader))
+					assert.Equal(t, tt.response.Body, haveBody)
+					assert.Equal(t, "application/x-snac", resp.Header.Get("Content-Type"))
+				} else {
+					assert.Equal(t, "text/plain; charset=utf-8", resp.Header.Get("Content-Type"))
+				}
 			}
 
 			assert.NoError(t, srv.Shutdown(context.Background()))
-
 			wg.Wait()
 		})
 	}