mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-21 08:09:55 +00:00
Adds a new "private" service mode for the reverse proxy: services reachable exclusively over the embedded WireGuard tunnel, gated by per-peer group membership instead of operator auth schemes. Wire contract - ProxyMapping.private (field 13): the proxy MUST call ValidateTunnelPeer and fail closed; operator schemes are bypassed. - ProxyCapabilities.private (4) + supports_private_service (5): capability gate. Management never streams private mappings to proxies that don't claim the capability; the broadcast path applies the same filter via filterMappingsForProxy. - ValidateTunnelPeer RPC: resolves an inbound tunnel IP to a peer, checks the peer's groups against service.AccessGroups, and mints a session JWT on success. checkPeerGroupAccess fails closed when a private service has empty AccessGroups. - ValidateSession/ValidateTunnelPeer responses now carry peer_group_ids + peer_group_names so the proxy can authorise policy-aware middlewares without an extra management round-trip. - ProxyInboundListener + SendStatusUpdate.inbound_listener: per-account inbound listener state surfaced to dashboards. - PathTargetOptions.direct_upstream (11): bypass the embedded NetBird client and dial the target via the proxy host's network stack for upstreams reachable without WireGuard. Data model - Service.Private (bool) + Service.AccessGroups ([]string, JSON- serialised). Validate() rejects bearer auth on private services. Copy() deep-copies AccessGroups. pgx getServices loads the columns. - DomainConfig.Private threaded into the proxy auth middleware. Request handler routes private services through forwardWithTunnelPeer and returns 403 on validation failure. - Account-level SynthesizePrivateServiceZones (synthetic DNS) and injectPrivateServicePolicies (synthetic ACL) gate on len(svc.AccessGroups) > 0. Proxy - /netbird proxy --private (embedded mode) flag; Config.Private in proxy/lifecycle.go. - Per-account inbound listener (proxy/inbound.go) binding HTTP/HTTPS on the embedded NetBird client's WireGuard tunnel netstack. - proxy/internal/auth/tunnel_cache: ValidateTunnelPeer response cache with single-flight de-duplication and per-account eviction. - Local peerstore short-circuit: when the inbound IP isn't in the account roster, deny fast without an RPC. - proxy/server.go reports SupportsPrivateService=true and redacts the full ProxyMapping JSON from info logs (auth_token + header-auth hashed values now only at debug level). Identity forwarding - ValidateSessionJWT returns user_id, email, method, groups, group_names. sessionkey.Claims carries Email + Groups + GroupNames so the proxy can stamp identity onto upstream requests without an extra management round-trip on every cookie-bearing request. - CapturedData carries userEmail / userGroups / userGroupNames; the proxy stamps X-NetBird-User and X-NetBird-Groups on r.Out from the authenticated identity (strips client-supplied values first to prevent spoofing). - AccessLog.UserGroups: access-log enrichment captures the user's group memberships at write time so the dashboard can render group context without reverse-resolving stale memberships. OpenAPI/dashboard surface - ReverseProxyService gains private + access_groups; ReverseProxyCluster gains private + supports_private. ReverseProxyTarget target_type enum gains "cluster". ServiceTargetOptions gains direct_upstream. ProxyAccessLog gains user_groups.
161 lines
4.6 KiB
Go
161 lines
4.6 KiB
Go
package accesslogs
|
|
|
|
import (
|
|
"maps"
|
|
"net"
|
|
"net/netip"
|
|
"time"
|
|
|
|
"github.com/netbirdio/netbird/management/server/peer"
|
|
"github.com/netbirdio/netbird/shared/management/http/api"
|
|
"github.com/netbirdio/netbird/shared/management/proto"
|
|
)
|
|
|
|
// AccessLogProtocol identifies the transport protocol of an access log entry.
|
|
type AccessLogProtocol string
|
|
|
|
const (
|
|
AccessLogProtocolHTTP AccessLogProtocol = "http"
|
|
AccessLogProtocolTCP AccessLogProtocol = "tcp"
|
|
AccessLogProtocolUDP AccessLogProtocol = "udp"
|
|
)
|
|
|
|
type AccessLogEntry struct {
|
|
ID string `gorm:"primaryKey"`
|
|
AccountID string `gorm:"index"`
|
|
ServiceID string `gorm:"index"`
|
|
Timestamp time.Time `gorm:"index"`
|
|
GeoLocation peer.Location `gorm:"embedded;embeddedPrefix:location_"`
|
|
SubdivisionCode string
|
|
Method string `gorm:"index"`
|
|
Host string `gorm:"index"`
|
|
Path string `gorm:"index"`
|
|
Duration time.Duration `gorm:"index"`
|
|
StatusCode int `gorm:"index"`
|
|
Reason string
|
|
UserId string `gorm:"index"`
|
|
AuthMethodUsed string `gorm:"index"`
|
|
BytesUpload int64 `gorm:"index"`
|
|
BytesDownload int64 `gorm:"index"`
|
|
Protocol AccessLogProtocol `gorm:"index"`
|
|
Metadata map[string]string `gorm:"serializer:json"`
|
|
// UserGroups captures the group IDs the user (UserId) belonged to at
|
|
// the time the entry was written, so the dashboard can render group
|
|
// context without reverse-resolving stale memberships.
|
|
UserGroups []string `gorm:"serializer:json"`
|
|
}
|
|
|
|
// FromProto creates an AccessLogEntry from a proto.AccessLog
|
|
func (a *AccessLogEntry) FromProto(serviceLog *proto.AccessLog) {
|
|
a.ID = serviceLog.GetLogId()
|
|
a.ServiceID = serviceLog.GetServiceId()
|
|
a.Timestamp = serviceLog.GetTimestamp().AsTime()
|
|
a.Method = serviceLog.GetMethod()
|
|
a.Host = serviceLog.GetHost()
|
|
a.Path = serviceLog.GetPath()
|
|
a.Duration = time.Duration(serviceLog.GetDurationMs()) * time.Millisecond
|
|
a.StatusCode = int(serviceLog.GetResponseCode())
|
|
a.UserId = serviceLog.GetUserId()
|
|
a.AuthMethodUsed = serviceLog.GetAuthMechanism()
|
|
a.AccountID = serviceLog.GetAccountId()
|
|
a.BytesUpload = serviceLog.GetBytesUpload()
|
|
a.BytesDownload = serviceLog.GetBytesDownload()
|
|
a.Protocol = AccessLogProtocol(serviceLog.GetProtocol())
|
|
a.Metadata = maps.Clone(serviceLog.GetMetadata())
|
|
|
|
if sourceIP := serviceLog.GetSourceIp(); sourceIP != "" {
|
|
if addr, err := netip.ParseAddr(sourceIP); err == nil {
|
|
addr = addr.Unmap()
|
|
a.GeoLocation.ConnectionIP = net.IP(addr.AsSlice())
|
|
}
|
|
}
|
|
|
|
// Only set reason for HTTP entries. L4 entries have no auth or status code.
|
|
if a.Protocol == "" || a.Protocol == AccessLogProtocolHTTP {
|
|
if !serviceLog.GetAuthSuccess() {
|
|
a.Reason = "Authentication failed"
|
|
} else if serviceLog.GetResponseCode() >= 400 {
|
|
a.Reason = "Request failed"
|
|
}
|
|
}
|
|
}
|
|
|
|
// ToAPIResponse converts an AccessLogEntry to the API ProxyAccessLog type
|
|
func (a *AccessLogEntry) ToAPIResponse() *api.ProxyAccessLog {
|
|
var sourceIP *string
|
|
if a.GeoLocation.ConnectionIP != nil {
|
|
ip := a.GeoLocation.ConnectionIP.String()
|
|
sourceIP = &ip
|
|
}
|
|
|
|
var reason *string
|
|
if a.Reason != "" {
|
|
reason = &a.Reason
|
|
}
|
|
|
|
var userID *string
|
|
if a.UserId != "" {
|
|
userID = &a.UserId
|
|
}
|
|
|
|
var authMethod *string
|
|
if a.AuthMethodUsed != "" {
|
|
authMethod = &a.AuthMethodUsed
|
|
}
|
|
|
|
var countryCode *string
|
|
if a.GeoLocation.CountryCode != "" {
|
|
countryCode = &a.GeoLocation.CountryCode
|
|
}
|
|
|
|
var cityName *string
|
|
if a.GeoLocation.CityName != "" {
|
|
cityName = &a.GeoLocation.CityName
|
|
}
|
|
|
|
var subdivisionCode *string
|
|
if a.SubdivisionCode != "" {
|
|
subdivisionCode = &a.SubdivisionCode
|
|
}
|
|
|
|
var protocol *string
|
|
if a.Protocol != "" {
|
|
p := string(a.Protocol)
|
|
protocol = &p
|
|
}
|
|
|
|
var metadata *map[string]string
|
|
if len(a.Metadata) > 0 {
|
|
metadata = &a.Metadata
|
|
}
|
|
|
|
var userGroups *[]string
|
|
if len(a.UserGroups) > 0 {
|
|
groups := append([]string(nil), a.UserGroups...)
|
|
userGroups = &groups
|
|
}
|
|
|
|
return &api.ProxyAccessLog{
|
|
Id: a.ID,
|
|
ServiceId: a.ServiceID,
|
|
Timestamp: a.Timestamp,
|
|
Method: a.Method,
|
|
Host: a.Host,
|
|
Path: a.Path,
|
|
DurationMs: int(a.Duration.Milliseconds()),
|
|
StatusCode: a.StatusCode,
|
|
SourceIp: sourceIP,
|
|
Reason: reason,
|
|
UserId: userID,
|
|
AuthMethodUsed: authMethod,
|
|
CountryCode: countryCode,
|
|
CityName: cityName,
|
|
SubdivisionCode: subdivisionCode,
|
|
BytesUpload: a.BytesUpload,
|
|
BytesDownload: a.BytesDownload,
|
|
Protocol: protocol,
|
|
Metadata: metadata,
|
|
UserGroups: userGroups,
|
|
}
|
|
}
|