docs: update README with desktop requirements, helper builds, and realistic MVP usage notes

Expand README with desktop platform requirements (Windows x86, macOS ARM), helper build commands, gateway utility scripts, and updated local test flow. Add realistic MVP usage section clarifying current platform build status, gateway configuration needs, and admin debug profile behavior with client private key handling.
This commit is contained in:
2026-03-16 06:30:08 +01:00
parent 7c4bba1021
commit 6ec5133773
32 changed files with 1076 additions and 49 deletions

View File

@@ -96,11 +96,19 @@ func (s *Service) ListAll(ctx context.Context) ([]Device, error) {
}
func (s *Service) GetLatestEnrollmentByUser(ctx context.Context, userID uuid.UUID) (EnrollmentResponse, error) {
return s.repo.GetLatestEnrollmentByUser(ctx, userID)
enrollment, err := s.repo.GetLatestEnrollmentByUser(ctx, userID)
if err != nil {
return EnrollmentResponse{}, err
}
return withDebugProfile(enrollment), nil
}
func (s *Service) GetEnrollmentByDeviceID(ctx context.Context, deviceID uuid.UUID) (EnrollmentResponse, error) {
return s.repo.GetEnrollmentByDeviceID(ctx, deviceID)
enrollment, err := s.repo.GetEnrollmentByDeviceID(ctx, deviceID)
if err != nil {
return EnrollmentResponse{}, err
}
return withDebugProfile(enrollment), nil
}
func (s *Service) GetConnectionStatus(ctx context.Context, userID uuid.UUID) (ConnectionStatus, error) {
@@ -128,3 +136,19 @@ func (s *Service) Revoke(ctx context.Context, deviceID uuid.UUID) error {
func (s *Service) Rotate(ctx context.Context, deviceID uuid.UUID) error {
return s.repo.Rotate(ctx, deviceID)
}
func withDebugProfile(enrollment EnrollmentResponse) EnrollmentResponse {
enrollment.Profile = ProfileView{
Format: "wireguard",
Content: profile.BuildWireGuardConfig(profile.BuildInput{
PrivateKey: "__CLIENT_PRIVATE_KEY_REQUIRED__",
Address: enrollment.Peer.AssignedIP,
DNSServers: enrollment.Peer.DNSServers,
ServerPublicKey: enrollment.Peer.Gateway.PublicKey,
ServerEndpoint: enrollment.Peer.Gateway.Endpoint,
AllowedIPs: enrollment.Peer.AllowedIPs,
PersistentKeepal: 25,
}),
}
return enrollment
}

View File

@@ -1,6 +1,7 @@
package gateway
import (
"encoding/json"
"net/http"
"github.com/go-chi/chi/v5"
@@ -35,3 +36,19 @@ func (h *Handler) SyncBundle(w http.ResponseWriter, r *http.Request) {
apiutil.JSON(w, http.StatusOK, bundle)
}
func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
var input UpdateRequest
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
apiutil.Error(w, http.StatusBadRequest, "invalid_json", "invalid request body")
return
}
item, err := h.service.Update(r.Context(), chi.URLParam(r, "id"), input)
if err != nil {
apiutil.Error(w, http.StatusBadRequest, "gateway_update_failed", "unable to update gateway")
return
}
apiutil.JSON(w, http.StatusOK, item)
}

View File

@@ -2,6 +2,7 @@ package gateway
import (
"context"
"net/netip"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
@@ -13,6 +14,7 @@ type Repository interface {
List(ctx context.Context) ([]Gateway, error)
FirstActive(ctx context.Context) (Gateway, error)
BuildSyncBundle(ctx context.Context, gatewayID uuid.UUID) (wireguard.GatewayBundle, error)
Update(ctx context.Context, gatewayID uuid.UUID, input UpdateRequest) (Gateway, error)
}
type PGRepository struct {
@@ -66,19 +68,33 @@ func (r *PGRepository) BuildSyncBundle(ctx context.Context, gatewayID uuid.UUID)
bundle.Revision = 1
row := r.db.QueryRow(ctx, `
select host(vpn_cidr), listen_port
select vpn_cidr::text, listen_port
from gateways
where id = $1 and deleted_at is null
`, gatewayID)
if err := row.Scan(&bundle.Interface.Address, &bundle.Interface.ListenPort); err != nil {
var vpnCIDR string
if err := row.Scan(&vpnCIDR, &bundle.Interface.ListenPort); err != nil {
return wireguard.GatewayBundle{}, err
}
interfaceAddress, err := gatewayInterfaceAddress(vpnCIDR)
if err != nil {
return wireguard.GatewayBundle{}, err
}
bundle.Interface.Address = interfaceAddress
bundle.Interface.NetworkCIDR = vpnCIDR
rows, err := r.db.Query(ctx, `
select d.id, wp.public_key, host(wp.assigned_ip), coalesce(array_agg(pd.destination::text) filter (where pd.destination is not null), '{}')
select
d.id,
wp.public_key,
set_masklen(wp.assigned_ip, 32)::text,
coalesce(array_agg(distinct pd.destination::text) filter (where pd.destination is not null), '{}')
from devices d
join wireguard_peers wp on wp.device_id = d.id and wp.deleted_at is null
left join policy_targets pt on pt.target_id = d.id and pt.target_type = 'device'
left join policy_targets pt on (
(pt.target_type = 'device' and pt.target_id = d.id) or
(pt.target_type = 'user' and pt.target_id = d.user_id)
)
left join policy_destinations pd on pd.policy_id = pt.policy_id
where d.gateway_id = $1 and d.deleted_at is null and d.status = 'active'
group by d.id, wp.public_key, wp.assigned_ip
@@ -100,3 +116,46 @@ func (r *PGRepository) BuildSyncBundle(ctx context.Context, gatewayID uuid.UUID)
return bundle, rows.Err()
}
func (r *PGRepository) Update(ctx context.Context, gatewayID uuid.UUID, input UpdateRequest) (Gateway, error) {
row := r.db.QueryRow(ctx, `
update gateways
set endpoint = $2,
public_key = $3,
listen_port = $4,
vpn_cidr = $5::cidr,
dns_servers = $6::text[],
is_active = $7,
updated_at = now()
where id = $1
returning id, name, endpoint, public_key, listen_port, vpn_cidr, dns_servers, is_active
`, gatewayID, input.Endpoint, input.PublicKey, input.ListenPort, input.VPNCIDR, input.DNSServers, input.IsActive)
var item Gateway
err := row.Scan(&item.ID, &item.Name, &item.Endpoint, &item.PublicKey, &item.ListenPort, &item.VPNCIDR, &item.DNSServers, &item.IsActive)
return item, err
}
func gatewayInterfaceAddress(cidr string) (string, error) {
prefix, err := netip.ParsePrefix(cidr)
if err != nil {
return "", err
}
return prefix.Addr().Next().String() + "/" + intToString(prefix.Bits()), nil
}
func intToString(value int) string {
if value == 0 {
return "0"
}
var digits [20]byte
index := len(digits)
for value > 0 {
index--
digits[index] = byte('0' + value%10)
value /= 10
}
return string(digits[index:])
}

View File

@@ -31,3 +31,11 @@ func (s *Service) BuildSyncBundle(ctx context.Context, gatewayID string) (wiregu
}
return s.repo.BuildSyncBundle(ctx, id)
}
func (s *Service) Update(ctx context.Context, gatewayID string, input UpdateRequest) (Gateway, error) {
id, err := uuid.Parse(gatewayID)
if err != nil {
return Gateway{}, err
}
return s.repo.Update(ctx, id, input)
}

View File

@@ -12,3 +12,12 @@ type Gateway struct {
DNSServers []string `json:"dns_servers"`
IsActive bool `json:"is_active"`
}
type UpdateRequest struct {
Endpoint string `json:"endpoint"`
PublicKey string `json:"public_key"`
ListenPort int `json:"listen_port"`
VPNCIDR string `json:"vpn_cidr"`
DNSServers []string `json:"dns_servers"`
IsActive bool `json:"is_active"`
}

View File

@@ -59,6 +59,7 @@ func NewRouter(jwtSecret string, handlers Handlers) http.Handler {
r.Post("/policies", handlers.Policy.Create)
r.Get("/gateways", handlers.Gateway.List)
r.Get("/gateways/{id}/sync", handlers.Gateway.SyncBundle)
r.Patch("/gateways/{id}", handlers.Gateway.Update)
r.Get("/audit-logs", handlers.Audit.List)
})
})

View File

@@ -12,6 +12,7 @@ type GatewayBundle struct {
Revision int `json:"revision"`
Interface struct {
Address string `json:"address"`
NetworkCIDR string `json:"network_cidr"`
ListenPort int `json:"listen_port"`
} `json:"interface"`
Peers []Peer `json:"peers"`