mirror of
https://github.com/ultravioletrs/cocos.git
synced 2026-06-23 04:10:25 +00:00
a3265bc346
* feat: Introduce computation runner, log forwarder, ingress, and egress proxy services. Signed-off-by: Sammy Oina <sammyoina@gmail.com> * feat: Update Go environment variable parsing and build system to use new architecture and repository. Signed-off-by: Sammy Oina <sammyoina@gmail.com> * feat: Update package sources to `sammyoina/cocos-ai` at a specific commit, add log-forwarder pre-start hook, and rename proxy binaries. Signed-off-by: Sammy Oina <sammyoina@gmail.com> * chore: Update build system references to a specific commit and enhance logging for service connections and message processing. Signed-off-by: Sammy Oina <sammyoina@gmail.com> * build: Update package source repositories and versions, migrate client logging to slog, and adjust ingress/egress proxy build and install steps. Signed-off-by: Sammy Oina <sammyoina@gmail.com> * debug stuck Signed-off-by: Sammy Oina <sammyoina@gmail.com> * debug Signed-off-by: Sammy Oina <sammyoina@gmail.com> * debug Signed-off-by: Sammy Oina <sammyoina@gmail.com> * feat: add HTTP/2 support to egress proxy and update build system to use specific commit hashes Signed-off-by: Sammy Oina <sammyoina@gmail.com> * feat: enhance egress proxy CONNECT handling, update package sources, and add gRPC test utility Signed-off-by: Sammy Oina <sammyoina@gmail.com> * feat: Update build system for various services to a specific commit from a new repository, change agent gRPC port to 7001, and add a gRPC test client. Signed-off-by: Sammy Oina <sammyoina@gmail.com> * feat: Migrate agent-internal gRPC communication to Unix sockets, set ingress proxy to port 7002, and update build hashes. Signed-off-by: Sammy Oina <sammyoina@gmail.com> * refactor: Remove standalone ingress-proxy systemd service and update component versions. Signed-off-by: Sammy Oina <sammyoina@gmail.com> * fix: Prevent computation re-initialization in agent and update component versions across several packages. Signed-off-by: Sammy Oina <sammyoina@gmail.com> * feat: update package versions and enable h2c support in ingress proxy. Signed-off-by: Sammy Oina <sammyoina@gmail.com> * feat: refactor ingress proxy to support HTTP/2 over Unix sockets and update component versions. Signed-off-by: Sammy Oina <sammyoina@gmail.com> * feat: Update build system package sources to `ultravioletrs/cocos` and reduce agent logging verbosity. Signed-off-by: Sammy Oina <sammyoina@gmail.com> * refactor: improve error handling in proxy commands and remove unused gRPC test Signed-off-by: Sammy Oina <sammyoina@gmail.com> * test: add mock service state return value in handleRunReqChunks test Signed-off-by: Sammy Oina <sammyoina@gmail.com> * feat: add comprehensive tests for service and proxy components Signed-off-by: Sammy Oina <sammyoina@gmail.com> * fix linter Signed-off-by: Sammy Oina <sammyoina@gmail.com> * improve coverage Signed-off-by: Sammy Oina <sammyoina@gmail.com> * test: add gRPC client and ingress adapter tests, and update egress proxy tests. Signed-off-by: Sammy Oina <sammyoina@gmail.com> * improve coverage Signed-off-by: Sammy Oina <sammyoina@gmail.com> --------- Signed-off-by: Sammy Oina <sammyoina@gmail.com>
478 lines
13 KiB
Go
478 lines
13 KiB
Go
// Copyright (c) Ultraviolet
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
package ingress
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"log/slog"
|
|
"math/big"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/ultravioletrs/cocos/pkg/atls/mocks"
|
|
"golang.org/x/net/http2"
|
|
"golang.org/x/net/http2/h2c"
|
|
)
|
|
|
|
func createTempCert(t *testing.T) (certFile, keyFile string) {
|
|
t.Helper()
|
|
|
|
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
require.NoError(t, err)
|
|
|
|
template := x509.Certificate{
|
|
SerialNumber: big.NewInt(1),
|
|
Subject: pkix.Name{
|
|
Organization: []string{"Test Org"},
|
|
},
|
|
NotBefore: time.Now(),
|
|
NotAfter: time.Now().Add(time.Hour),
|
|
|
|
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
BasicConstraintsValid: true,
|
|
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
|
|
}
|
|
|
|
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
|
require.NoError(t, err)
|
|
|
|
tmpDir := t.TempDir()
|
|
certPath := filepath.Join(tmpDir, "server.crt")
|
|
keyPath := filepath.Join(tmpDir, "server.key")
|
|
|
|
certOut, err := os.Create(certPath)
|
|
require.NoError(t, err)
|
|
err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
|
require.NoError(t, err)
|
|
certOut.Close()
|
|
|
|
keyOut, err := os.Create(keyPath)
|
|
require.NoError(t, err)
|
|
b, err := x509.MarshalECPrivateKey(priv)
|
|
require.NoError(t, err)
|
|
err = pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: b})
|
|
require.NoError(t, err)
|
|
keyOut.Close()
|
|
|
|
return certPath, keyPath
|
|
}
|
|
|
|
func getBackendURL() *url.URL {
|
|
u, _ := url.Parse("http://localhost:8080")
|
|
return u
|
|
}
|
|
|
|
// TestNewProxyServer tests the creation of a new proxy server.
|
|
func TestNewProxyServer(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
|
ps := NewProxyServer(logger, getBackendURL(), nil)
|
|
require.NotNil(t, ps)
|
|
}
|
|
|
|
// TestProxyStartStop tests basic start and stop operations.
|
|
func TestProxyStartStop(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
|
ps := NewProxyServer(logger, getBackendURL(), nil)
|
|
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
port := listener.Addr().(*net.TCPAddr).Port
|
|
listener.Close()
|
|
|
|
cfg := ProxyConfig{Port: fmt.Sprintf("%d", port)}
|
|
ctx := ProxyContext{ID: "test-1", Name: "test-proxy"}
|
|
|
|
err = ps.Start(cfg, ctx)
|
|
require.NoError(t, err)
|
|
time.Sleep(100 * time.Millisecond)
|
|
err = ps.Stop()
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// TestProxyStartWithoutPort tests proxy without explicit port.
|
|
func TestProxyStartWithoutPort(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
|
ps := NewProxyServer(logger, getBackendURL(), nil)
|
|
|
|
cfg := ProxyConfig{Port: ""}
|
|
ctx := ProxyContext{ID: "test-2"}
|
|
|
|
err := ps.Start(cfg, ctx)
|
|
require.NoError(t, err)
|
|
defer func() { _ = ps.Stop() }()
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
|
|
// TestProxyStartAlreadyStarted tests error when starting twice.
|
|
func TestProxyStartAlreadyStarted(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
|
ps := NewProxyServer(logger, getBackendURL(), nil)
|
|
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
port := listener.Addr().(*net.TCPAddr).Port
|
|
listener.Close()
|
|
|
|
cfg := ProxyConfig{Port: fmt.Sprintf("%d", port)}
|
|
ctx := ProxyContext{ID: "test-3"}
|
|
|
|
err = ps.Start(cfg, ctx)
|
|
require.NoError(t, err)
|
|
defer func() { _ = ps.Stop() }()
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
err = ps.Start(cfg, ctx)
|
|
assert.Error(t, err)
|
|
assert.Equal(t, "proxy server already started", err.Error())
|
|
}
|
|
|
|
// TestProxyStartAfterStopped tests error when starting after stop.
|
|
func TestProxyStartAfterStopped(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
|
ps := NewProxyServer(logger, getBackendURL(), nil)
|
|
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
port := listener.Addr().(*net.TCPAddr).Port
|
|
listener.Close()
|
|
|
|
cfg := ProxyConfig{Port: fmt.Sprintf("%d", port)}
|
|
ctx := ProxyContext{ID: "test-4"}
|
|
|
|
err = ps.Start(cfg, ctx)
|
|
require.NoError(t, err)
|
|
time.Sleep(100 * time.Millisecond)
|
|
err = ps.Stop()
|
|
require.NoError(t, err)
|
|
|
|
err = ps.Start(cfg, ctx)
|
|
assert.Error(t, err)
|
|
// After stop, attempts to start will fail with "already started" error first
|
|
assert.Contains(t, err.Error(), "proxy server already")
|
|
}
|
|
|
|
// TestProxyWithName tests proxy context with name.
|
|
func TestProxyWithName(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
|
ps := NewProxyServer(logger, getBackendURL(), nil)
|
|
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
port := listener.Addr().(*net.TCPAddr).Port
|
|
listener.Close()
|
|
|
|
cfg := ProxyConfig{Port: fmt.Sprintf("%d", port)}
|
|
ctx := ProxyContext{ID: "id-1", Name: "named-proxy"}
|
|
|
|
err = ps.Start(cfg, ctx)
|
|
require.NoError(t, err)
|
|
defer func() { _ = ps.Stop() }()
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
|
|
// TestProxyWithoutName tests proxy context without name.
|
|
func TestProxyWithoutName(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
|
ps := NewProxyServer(logger, getBackendURL(), nil)
|
|
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
port := listener.Addr().(*net.TCPAddr).Port
|
|
listener.Close()
|
|
|
|
cfg := ProxyConfig{Port: fmt.Sprintf("%d", port)}
|
|
ctx := ProxyContext{ID: "id-only"}
|
|
|
|
err = ps.Start(cfg, ctx)
|
|
require.NoError(t, err)
|
|
defer func() { _ = ps.Stop() }()
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
|
|
// TestProxyMultipleStops tests multiple stop calls.
|
|
func TestProxyMultipleStops(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
|
ps := NewProxyServer(logger, getBackendURL(), nil)
|
|
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
port := listener.Addr().(*net.TCPAddr).Port
|
|
listener.Close()
|
|
|
|
cfg := ProxyConfig{Port: fmt.Sprintf("%d", port)}
|
|
ctx := ProxyContext{ID: "test-multi-stop"}
|
|
|
|
err = ps.Start(cfg, ctx)
|
|
require.NoError(t, err)
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
err = ps.Stop()
|
|
require.NoError(t, err)
|
|
|
|
err = ps.Stop()
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// TestProxyWithoutTLS tests proxy without TLS.
|
|
func TestProxyWithoutTLS(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
|
ps := NewProxyServer(logger, getBackendURL(), nil)
|
|
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
port := listener.Addr().(*net.TCPAddr).Port
|
|
listener.Close()
|
|
|
|
cfg := ProxyConfig{
|
|
Port: fmt.Sprintf("%d", port),
|
|
AttestedTLS: false,
|
|
}
|
|
ctx := ProxyContext{ID: "test-no-tls"}
|
|
|
|
err = ps.Start(cfg, ctx)
|
|
require.NoError(t, err)
|
|
defer func() { _ = ps.Stop() }()
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
|
|
func TestProxyWithUnixBackend(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
|
|
|
// Create a temp directory for socket
|
|
dir := t.TempDir()
|
|
sockPath := filepath.Join(dir, "backend.sock")
|
|
|
|
// Start a dummy backend on unix socket
|
|
l, err := net.Listen("unix", sockPath)
|
|
require.NoError(t, err)
|
|
defer l.Close()
|
|
|
|
backendCalled := false
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
backendCalled = true
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
h2s := &http2.Server{}
|
|
h2cHandler := h2c.NewHandler(handler, h2s)
|
|
|
|
go func() {
|
|
_ = http.Serve(l, h2cHandler)
|
|
}()
|
|
|
|
// Configure proxy to use this unix socket
|
|
backendURL, _ := url.Parse("unix://" + sockPath)
|
|
ps := NewProxyServer(logger, backendURL, nil)
|
|
|
|
// Find free port for proxy
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
port := listener.Addr().(*net.TCPAddr).Port
|
|
listener.Close()
|
|
|
|
cfg := ProxyConfig{
|
|
Port: fmt.Sprintf("%d", port),
|
|
}
|
|
ctx := ProxyContext{ID: "test-unix"}
|
|
|
|
err = ps.Start(cfg, ctx)
|
|
require.NoError(t, err)
|
|
defer func() { _ = ps.Stop() }()
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Make request to proxy
|
|
resp, err := http.Get(fmt.Sprintf("http://localhost:%d", port))
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
assert.True(t, backendCalled)
|
|
}
|
|
|
|
func TestProxyRegularTLS(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
|
certFile, keyFile := createTempCert(t)
|
|
|
|
ps := NewProxyServer(logger, getBackendURL(), nil)
|
|
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
port := listener.Addr().(*net.TCPAddr).Port
|
|
listener.Close()
|
|
|
|
cfg := ProxyConfig{
|
|
Port: fmt.Sprintf("%d", port),
|
|
CertFile: certFile,
|
|
KeyFile: keyFile,
|
|
}
|
|
ctx := ProxyContext{ID: "test-tls"}
|
|
|
|
err = ps.Start(cfg, ctx)
|
|
require.NoError(t, err)
|
|
defer func() { _ = ps.Stop() }()
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Client with skipped verification
|
|
tr := &http.Transport{
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
}
|
|
client := &http.Client{Transport: tr}
|
|
|
|
resp, err := client.Get(fmt.Sprintf("https://localhost:%d", port))
|
|
// Backend is not running/reachable so 502 or error is expected from reverse proxy,
|
|
// but 502 means connection to proxy succeeded.
|
|
// If the proxy itself was not working, we'd get connection refused or similar.
|
|
require.NoError(t, err)
|
|
if err == nil {
|
|
resp.Body.Close()
|
|
}
|
|
}
|
|
|
|
func TestProxyRegularTLSInvalidFiles(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
|
ps := NewProxyServer(logger, getBackendURL(), nil)
|
|
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
port := listener.Addr().(*net.TCPAddr).Port
|
|
listener.Close()
|
|
|
|
cfg := ProxyConfig{
|
|
Port: fmt.Sprintf("%d", port),
|
|
CertFile: "non-existent.crt",
|
|
KeyFile: "non-existent.key",
|
|
}
|
|
ctx := ProxyContext{ID: "test-tls-fail"}
|
|
|
|
err = ps.Start(cfg, ctx)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "failed to setup TLS")
|
|
}
|
|
|
|
func TestProxyAttestedTLS(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
|
mockProvider := mocks.NewCertificateProvider(t)
|
|
// We don't expect calls during Listen, only during handshake.
|
|
// But Start logic doesn't block waiting for handshake.
|
|
|
|
ps := NewProxyServer(logger, getBackendURL(), mockProvider)
|
|
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
port := listener.Addr().(*net.TCPAddr).Port
|
|
listener.Close()
|
|
|
|
cfg := ProxyConfig{
|
|
Port: fmt.Sprintf("%d", port),
|
|
AttestedTLS: true,
|
|
}
|
|
ctx := ProxyContext{ID: "test-attested-tls"}
|
|
|
|
err = ps.Start(cfg, ctx)
|
|
require.NoError(t, err)
|
|
defer func() { _ = ps.Stop() }()
|
|
}
|
|
|
|
func TestProxyAttestedTLSMissingProvider(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
|
ps := NewProxyServer(logger, getBackendURL(), nil)
|
|
|
|
cfg := ProxyConfig{
|
|
Port: "0",
|
|
AttestedTLS: true,
|
|
}
|
|
ctx := ProxyContext{ID: "test-attested-fail"}
|
|
|
|
err := ps.Start(cfg, ctx)
|
|
assert.Error(t, err)
|
|
assert.Equal(t, "attested TLS requested but no certificate provider available", err.Error())
|
|
}
|
|
|
|
func TestProxyMTLS(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
|
certFile, _ := createTempCert(t)
|
|
|
|
mockProvider := mocks.NewCertificateProvider(t)
|
|
|
|
ps := NewProxyServer(logger, getBackendURL(), mockProvider)
|
|
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
port := listener.Addr().(*net.TCPAddr).Port
|
|
listener.Close()
|
|
|
|
// Test case: AttestedTLS with ClientCAFile (mTLS)
|
|
// server.ConfigureCertificateAuthorities reads the file.
|
|
cfg := ProxyConfig{
|
|
Port: fmt.Sprintf("%d", port),
|
|
AttestedTLS: true,
|
|
ClientCAFile: certFile, // Use self-signed cert as CA
|
|
ServerCAFile: certFile, // Also for server CA
|
|
}
|
|
ctx := ProxyContext{ID: "test-mtls"}
|
|
|
|
err = ps.Start(cfg, ctx)
|
|
require.NoError(t, err)
|
|
defer func() { _ = ps.Stop() }()
|
|
}
|
|
|
|
func TestProxyRegularMTLS(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
|
certFile, keyFile := createTempCert(t)
|
|
|
|
ps := NewProxyServer(logger, getBackendURL(), nil)
|
|
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
port := listener.Addr().(*net.TCPAddr).Port
|
|
listener.Close()
|
|
|
|
cfg := ProxyConfig{
|
|
Port: fmt.Sprintf("%d", port),
|
|
CertFile: certFile,
|
|
KeyFile: keyFile,
|
|
ServerCAFile: certFile,
|
|
ClientCAFile: certFile,
|
|
}
|
|
ctx := ProxyContext{ID: "test-regular-mtls"}
|
|
|
|
err = ps.Start(cfg, ctx)
|
|
require.NoError(t, err)
|
|
defer func() { _ = ps.Stop() }()
|
|
}
|
|
|
|
func TestProxyAttestedTLSInvalidCA(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
|
mockProvider := mocks.NewCertificateProvider(t)
|
|
|
|
ps := NewProxyServer(logger, getBackendURL(), mockProvider)
|
|
|
|
cfg := ProxyConfig{
|
|
Port: "0",
|
|
AttestedTLS: true,
|
|
ServerCAFile: "non-existent.pem",
|
|
}
|
|
ctx := ProxyContext{ID: "test-attested-invalid-ca"}
|
|
|
|
err := ps.Start(cfg, ctx)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "failed to configure certificate authorities")
|
|
}
|