feat: add VPN DNS service with dynamic service catalog resolution and CoreDNS integration
Add ServiceDNSRecord type and gateway API endpoint to expose active service domain-to-IP mappings. Implement ListServiceDNSRecords repository method querying services table with proxy_ip resolution using effectiveAccessProxyIP helper. Add vpn-dns microservice built on CoreDNS with periodic sync from backend API. Generate Corefile with configurable upstream DNS servers and hosts plugin for service overrides.
This commit is contained in:
@@ -90,6 +90,21 @@ func (h *Handler) AgentSyncBundle(w http.ResponseWriter, r *http.Request) {
|
||||
apiutil.JSON(w, http.StatusOK, bundle)
|
||||
}
|
||||
|
||||
func (h *Handler) AgentServiceDNS(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("X-Gateway-Bootstrap-Token") != h.bootstrapToken {
|
||||
apiutil.Error(w, http.StatusUnauthorized, "unauthorized", "invalid gateway bootstrap token")
|
||||
return
|
||||
}
|
||||
|
||||
items, err := h.service.ListServiceDNSRecords(r.Context())
|
||||
if err != nil {
|
||||
apiutil.Error(w, http.StatusInternalServerError, "service_dns_failed", "unable to build service dns records")
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(w, http.StatusOK, map[string]any{"records": items})
|
||||
}
|
||||
|
||||
func (h *Handler) Telemetry(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("X-Gateway-Bootstrap-Token") != h.bootstrapToken {
|
||||
apiutil.Error(w, http.StatusUnauthorized, "unauthorized", "invalid gateway bootstrap token")
|
||||
|
||||
@@ -17,6 +17,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)
|
||||
ListServiceDNSRecords(ctx context.Context) ([]ServiceDNSRecord, error)
|
||||
StoreTelemetry(ctx context.Context, gatewayID uuid.UUID, snapshot TelemetrySnapshot) error
|
||||
Update(ctx context.Context, gatewayID uuid.UUID, input UpdateRequest) (Gateway, error)
|
||||
UpsertByName(ctx context.Context, input BootstrapRequest) (Gateway, error)
|
||||
@@ -192,6 +193,33 @@ func effectiveAccessProxyIP(proxyIP string) string {
|
||||
return proxyIP
|
||||
}
|
||||
|
||||
func (r *PGRepository) ListServiceDNSRecords(ctx context.Context) ([]ServiceDNSRecord, error) {
|
||||
rows, err := r.db.Query(ctx, `
|
||||
select distinct
|
||||
s.domain,
|
||||
host(s.proxy_ip)
|
||||
from services s
|
||||
where s.deleted_at is null and s.is_active = true
|
||||
order by s.domain asc
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var items []ServiceDNSRecord
|
||||
for rows.Next() {
|
||||
var item ServiceDNSRecord
|
||||
var proxyIP string
|
||||
if err := rows.Scan(&item.Domain, &proxyIP); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item.TargetIP = effectiveAccessProxyIP(proxyIP)
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, rows.Err()
|
||||
}
|
||||
|
||||
func (r *PGRepository) Update(ctx context.Context, gatewayID uuid.UUID, input UpdateRequest) (Gateway, error) {
|
||||
row := r.db.QueryRow(ctx, `
|
||||
update gateways
|
||||
|
||||
@@ -32,6 +32,10 @@ func (s *Service) BuildSyncBundle(ctx context.Context, gatewayID string) (wiregu
|
||||
return s.repo.BuildSyncBundle(ctx, id)
|
||||
}
|
||||
|
||||
func (s *Service) ListServiceDNSRecords(ctx context.Context) ([]ServiceDNSRecord, error) {
|
||||
return s.repo.ListServiceDNSRecords(ctx)
|
||||
}
|
||||
|
||||
func (s *Service) Update(ctx context.Context, gatewayID string, input UpdateRequest) (Gateway, error) {
|
||||
id, err := uuid.Parse(gatewayID)
|
||||
if err != nil {
|
||||
|
||||
6
backend/internal/gateway/types_dns.go
Normal file
6
backend/internal/gateway/types_dns.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package gateway
|
||||
|
||||
type ServiceDNSRecord struct {
|
||||
Domain string `json:"domain"`
|
||||
TargetIP string `json:"target_ip"`
|
||||
}
|
||||
Reference in New Issue
Block a user