diff --git a/deploy/upcloud/configs/cloudinit.sh.tmpl b/deploy/upcloud/configs/cloudinit.sh.tmpl index 89f4565..c767d1a 100644 --- a/deploy/upcloud/configs/cloudinit.sh.tmpl +++ b/deploy/upcloud/configs/cloudinit.sh.tmpl @@ -18,6 +18,7 @@ done export DEBIAN_FRONTEND=noninteractive apt-get update && apt-get upgrade -y apt-get install -y git gcc make curl libsqlite3-dev nodejs npm htop systemd-timesyncd +sed -i 's/^#NTP=.*/NTP=0.debian.pool.ntp.org 1.debian.pool.ntp.org 2.debian.pool.ntp.org 3.debian.pool.ntp.org/' /etc/systemd/timesyncd.conf timedatectl set-ntp true # Swap (for small instances) diff --git a/deploy/upcloud/provision.go b/deploy/upcloud/provision.go index fad9b5b..171e80c 100644 --- a/deploy/upcloud/provision.go +++ b/deploy/upcloud/provision.go @@ -646,10 +646,20 @@ func createFirewallRules(ctx context.Context, svc *service.Service, serverUUID, Position: 2, Comment: "Allow private network", }, + { + Direction: upcloud.FirewallRuleDirectionIn, + Action: upcloud.FirewallRuleActionAccept, + Family: upcloud.IPAddressFamilyIPv4, + Protocol: upcloud.FirewallRuleProtocolUDP, + SourcePortStart: "123", + SourcePortEnd: "123", + Position: 3, + Comment: "Allow NTP replies", + }, { Direction: upcloud.FirewallRuleDirectionIn, Action: upcloud.FirewallRuleActionDrop, - Position: 3, + Position: 4, Comment: "Drop all other inbound", }, }, diff --git a/pkg/atproto/relays.go b/pkg/atproto/relays.go index 714c4d7..77f4007 100644 --- a/pkg/atproto/relays.go +++ b/pkg/atproto/relays.go @@ -67,6 +67,7 @@ type RelayStatus struct { Online bool Error string HasRequestCrawl bool + RequestCrawlStatus int // HTTP status code from probe (400=open, 401/403=auth required, 5xx=error) HasListReposByCollection bool RepoStatus *RepoStatus HostStatus *HostStatus @@ -90,15 +91,14 @@ func CheckRelayStatus(relayURL, hostname, did string) *RelayStatus { // Probe requestCrawl go func() { defer wg.Done() - supported, online := probeRequestCrawl(relayURL) + supported, statusCode, online := probeRequestCrawl(relayURL) if online { markOnline() } - if supported { - mu.Lock() - result.HasRequestCrawl = true - mu.Unlock() - } + mu.Lock() + result.HasRequestCrawl = supported + result.RequestCrawlStatus = statusCode + mu.Unlock() }() // Check host status @@ -158,26 +158,31 @@ func CheckRelayStatus(relayURL, hostname, did string) *RelayStatus { return result } -// probeRequestCrawl checks if a relay supports the requestCrawl endpoint using a HEAD request. -// A 4xx response (e.g. 405 Method Not Allowed) means the endpoint exists. -// A 5xx or connection failure means it's broken or unsupported. -func probeRequestCrawl(relayURL string) (supported bool, online bool) { +// probeRequestCrawl checks if a relay supports the requestCrawl endpoint by POSTing +// an empty hostname. Returns (supported, statusCode, online): +// - 400 = endpoint exists and accepts unauthenticated crawls (supported=true) +// - 401/403 = endpoint exists but requires auth (supported=false) +// - 5xx = endpoint is broken (supported=false) +// - connection error = relay offline (online=false) +func probeRequestCrawl(relayURL string) (supported bool, statusCode int, online bool) { client := &http.Client{Timeout: 5 * time.Second} - req, err := http.NewRequest("HEAD", relayURL+SyncRequestCrawl, nil) + body := bytes.NewReader([]byte(`{"hostname":""}`)) + req, err := http.NewRequest("POST", relayURL+SyncRequestCrawl, body) if err != nil { - return false, false + return false, 0, false } + req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { - return false, false + return false, 0, false } defer resp.Body.Close() - // Any HTTP response means the relay is online. - // 4xx (typically 405 Method Not Allowed) = endpoint exists. - // 5xx = endpoint is broken. - return resp.StatusCode >= 400 && resp.StatusCode < 500, true + // 400 = endpoint exists, accepts unauthenticated requests (empty hostname rejected as expected) + // 401/403 = endpoint exists but requires authentication + // 5xx = endpoint is broken + return resp.StatusCode == http.StatusBadRequest, resp.StatusCode, true } // probeListReposByCollection checks if a relay supports the listReposByCollection endpoint. diff --git a/pkg/atproto/relays_test.go b/pkg/atproto/relays_test.go index d2b4cc4..66fbd5b 100644 --- a/pkg/atproto/relays_test.go +++ b/pkg/atproto/relays_test.go @@ -217,6 +217,8 @@ func TestCheckHostStatus(t *testing.T) { func TestCheckRelayStatus_AllEndpointsSucceed(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { + case SyncRequestCrawl: + w.WriteHeader(http.StatusBadRequest) // empty hostname = 400 case SyncGetHostStatus: json.NewEncoder(w).Encode(HostStatus{Hostname: "hold.example.com", Active: true}) case SyncGetRepoStatus: @@ -237,6 +239,12 @@ func TestCheckRelayStatus_AllEndpointsSucceed(t *testing.T) { if status.Error != "" { t.Errorf("expected no error, got %q", status.Error) } + if !status.HasRequestCrawl { + t.Error("expected HasRequestCrawl = true") + } + if status.RequestCrawlStatus != http.StatusBadRequest { + t.Errorf("RequestCrawlStatus = %d, want %d", status.RequestCrawlStatus, http.StatusBadRequest) + } if !status.HasListReposByCollection { t.Error("expected HasListReposByCollection = true") } @@ -257,6 +265,8 @@ func TestCheckRelayStatus_AllEndpointsSucceed(t *testing.T) { func TestCheckRelayStatus_OnlineButUnknownHost(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { + case SyncRequestCrawl: + w.WriteHeader(http.StatusBadRequest) // empty hostname = 400 case SyncGetHostStatus: w.WriteHeader(http.StatusBadRequest) // relay doesn't know this host case SyncGetRepoStatus: @@ -303,6 +313,8 @@ func TestCheckRelayStatus_Offline(t *testing.T) { func TestCheckRelayStatus_NoListReposByCollection(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { + case SyncRequestCrawl: + w.WriteHeader(http.StatusBadRequest) // empty hostname = 400 case SyncGetHostStatus: json.NewEncoder(w).Encode(HostStatus{Hostname: "hold.example.com", Active: true}) case SyncGetRepoStatus: @@ -327,3 +339,39 @@ func TestCheckRelayStatus_NoListReposByCollection(t *testing.T) { t.Error("expected RepoStatus to be active") } } + +func TestCheckRelayStatus_AuthRequired(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case SyncRequestCrawl: + w.WriteHeader(http.StatusForbidden) // auth required + case SyncGetHostStatus: + json.NewEncoder(w).Encode(HostStatus{Hostname: "hold.example.com", Active: true}) + case SyncGetRepoStatus: + json.NewEncoder(w).Encode(RepoStatus{DID: "did:web:hold.example.com", Active: true, Rev: "r1"}) + case SyncListReposByCollection: + json.NewEncoder(w).Encode(map[string]any{"repos": []any{}}) + default: + w.WriteHeader(http.StatusNotFound) + } + })) + defer srv.Close() + + status := CheckRelayStatus(srv.URL, "hold.example.com", "did:web:hold.example.com") + + if !status.Online { + t.Error("expected Online = true") + } + if status.HasRequestCrawl { + t.Error("expected HasRequestCrawl = false (auth required)") + } + if status.RequestCrawlStatus != http.StatusForbidden { + t.Errorf("RequestCrawlStatus = %d, want %d", status.RequestCrawlStatus, http.StatusForbidden) + } + if !status.HasListReposByCollection { + t.Error("expected HasListReposByCollection = true") + } + if status.RepoStatus == nil || !status.RepoStatus.Active { + t.Error("expected RepoStatus to be active") + } +} diff --git a/pkg/hold/admin/handlers_relays.go b/pkg/hold/admin/handlers_relays.go index 2c921d1..44a85fb 100644 --- a/pkg/hold/admin/handlers_relays.go +++ b/pkg/hold/admin/handlers_relays.go @@ -22,6 +22,7 @@ type RelayStatusView struct { Online bool Error string HasRequestCrawl bool + RequestCrawlStatus int HasListReposByCollection bool RepoStatus *atproto.RepoStatus HostStatus *atproto.HostStatus @@ -77,6 +78,7 @@ func (ui *AdminUI) handleRelayStatus(w http.ResponseWriter, r *http.Request) { Online: status.Online, Error: status.Error, HasRequestCrawl: status.HasRequestCrawl, + RequestCrawlStatus: status.RequestCrawlStatus, HasListReposByCollection: status.HasListReposByCollection, RepoStatus: status.RepoStatus, HostStatus: status.HostStatus, diff --git a/pkg/hold/admin/templates/partials/relay_status.html b/pkg/hold/admin/templates/partials/relay_status.html index dcbc406..7b84a69 100644 --- a/pkg/hold/admin/templates/partials/relay_status.html +++ b/pkg/hold/admin/templates/partials/relay_status.html @@ -22,7 +22,13 @@
{{if .Online}} - {{if .HasRequestCrawl}}requestCrawl{{end}} + {{if .HasRequestCrawl}} + requestCrawl + {{else if or (eq .RequestCrawlStatus 401) (eq .RequestCrawlStatus 403)}} + requestCrawl (auth required) + {{else if ge .RequestCrawlStatus 500}} + requestCrawl ({{.RequestCrawlStatus}}) + {{end}} {{if .HasListReposByCollection}}listReposByCollection{{end}} {{else}} -