Implementation for basic service access control (#95)

* Added ServicesACL config

Signed-off-by: Roland Urbano <urbano.roland@gmail.com>

* Adding filter to service receiver

Signed-off-by: Roland Urbano <urbano.roland@gmail.com>

* Implemented ACL handling in ServiceNetwork

Signed-off-by: Roland Urbano <roland.urbano@nts.eu>

* Updated Schema and added parsing for acl config

Signed-off-by: Roland Urbano <urbano.roland@gmail.com>

* Updated service package and fixed ACL handling

Signed-off-by: Roland Urbano <urbano.roland@gmail.com>

* go-jsonschema: 0.16.0 -> 0.22.0

* nixos: set acls on services directly

* config: move ServicesACL to Services

* svc: simplify acl logic

* remove whitespace

* apply nixfmt

---------

Signed-off-by: Roland Urbano <urbano.roland@gmail.com>
Signed-off-by: Roland Urbano <roland.urbano@nts.eu>
Co-authored-by: Max <max@privatevoid.net>
This commit is contained in:
Roland Urbano 2026-03-25 19:16:52 +01:00 committed by GitHub
parent ff94c7869e
commit ed30c60c95
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 130 additions and 26 deletions

View File

@ -2,6 +2,7 @@ package config
import (
"encoding/json"
"errors"
"fmt"
"log"
"net"
@ -18,15 +19,15 @@ import (
// Config is the main Configuration Struct for Hyprspace.
type Config struct {
Path string `json:"-"`
Interface string `json:"-"`
ListenAddresses []multiaddr.Multiaddr `json:"-"`
Peers []Peer `json:"peers"`
PeerLookup PeerLookup `json:"-"`
PrivateKey crypto.PrivKey `json:"-"`
BuiltinAddr4 net.IP `json:"-"`
BuiltinAddr6 net.IP `json:"-"`
Services map[string]multiaddr.Multiaddr `json:"-"`
Path string `json:"-"`
Interface string `json:"-"`
ListenAddresses []multiaddr.Multiaddr `json:"-"`
Peers []Peer `json:"peers"`
PeerLookup PeerLookup `json:"-"`
PrivateKey crypto.PrivKey `json:"-"`
BuiltinAddr4 net.IP `json:"-"`
BuiltinAddr6 net.IP `json:"-"`
Services map[string]Service `json:"-"`
}
// Peer defines a peer in the configuration. We might add more to this later.
@ -49,6 +50,17 @@ type RouteTableEntry struct {
Target Peer
}
// Service represents the configuration for a specific service provided by this node.
// Whitelist and Blacklist allow fine granularity in access control.
// If Blacklist is set, this will be evaluated first and any client id present in Blacklist will
// have access denied. Whitelist is evaluated after.
type Service struct {
Target multiaddr.Multiaddr
EnableWhitelist bool
Whitelist map[peer.ID]struct{}
Blacklist map[peer.ID]struct{}
}
func (rte RouteTableEntry) Network() net.IPNet {
return rte.Net
}
@ -144,13 +156,34 @@ func Read(path string) (*Config, error) {
result.Peers[i] = p
}
result.Services = make(map[string]multiaddr.Multiaddr)
for name, addrString := range input.Services {
addr, err := multiaddr.NewMultiaddr(addrString)
result.Services = make(map[string]Service)
for name, service := range input.Services {
addr, err := multiaddr.NewMultiaddr(service.Target)
if err != nil {
return nil, err
}
result.Services[name] = addr
whitelist := make(map[peer.ID]struct{})
blacklist := make(map[peer.ID]struct{})
for _, p := range service.Acl.Whitelist {
cfgPeer, found := FindPeerByCLIRef(result.Peers, p)
if !found {
return nil, errors.New("unknown peer: " + p)
}
whitelist[cfgPeer.ID] = struct{}{}
}
for _, peerStr := range service.Acl.Blacklist {
cfgPeer, found := FindPeerByCLIRef(result.Peers, peerStr)
if !found {
return nil, errors.New("unknown peer: " + peerStr)
}
blacklist[cfgPeer.ID] = struct{}{}
}
result.Services[name] = Service{
Target: addr,
EnableWhitelist: service.Acl.EnableWhitelist,
Whitelist: whitelist,
Blacklist: blacklist,
}
}
// Overwrite path of config to input.

View File

@ -6,16 +6,18 @@
buildGoModule rec {
pname = "go-jsonschema";
version = "0.16.0";
version = "0.22.0";
src = fetchFromGitHub {
owner = "omissis";
repo = "go-jsonschema";
rev = "v${version}";
hash = "sha256-+CapTmg4RObK6mzjAS/EFbX4s2AtQvlFXmT119aUkZA=";
hash = "sha256-ffrP4L5cfK75Tw/xfcdXAwGUP8WLL+81ltBDb/P5Gwo=";
};
vendorHash = "sha256-gk+aKGqcHEjuYxc2o+83HA2AxU+jT7URt0N/q+uyUtA=";
env.GOWORK = "off";
vendorHash = "sha256-mCOJ8GROrbNXH7CSLLMZj/4wTa65hscTt8RzIxzgG+A=";
ldflags = [
"-s"

View File

@ -1,7 +1,7 @@
{ lib, ... }:
let
inherit (lib) types mkOption;
inherit (lib) types mkOption mkEnableOption;
t = {
multiAddr = types.strMatching "/.*[^/]" // {
@ -42,6 +42,40 @@ let
};
};
};
service = types.submodule {
options = {
target = mkOption {
type = t.multiAddr;
description = "Target address.";
example = "/tcp/8080";
};
acl = {
enableWhitelist = mkEnableOption "whitelist enforcement";
whitelist = mkOption {
type = types.listOf types.str;
description = "List of peers that are allowed to connect.";
example = [
"12D3KooWQWiPeNvXFdHFTrustedPeer"
"@goodpeer"
];
default = null;
};
blacklist = mkOption {
type = types.listOf types.str;
description = "List of peers that are explicitly not allowed to connect.";
example = [
"12D3KooWQWiPeNvXFdHFUntrustedPeer"
"@badpeer"
];
default = null;
};
};
};
};
};
in
@ -82,12 +116,23 @@ in
};
services = mkOption {
type = types.attrsOf t.multiAddr;
type = types.attrsOf t.service;
description = "The services this node provides via the Service Network.";
default = { };
example = {
"www-local" = "/tcp/8080";
"gameserver" = "/ip4/10.0.0.2/tcp/27015";
www-local = {
target = "/tcp/8080";
};
gameserver = {
target = "/ip4/10.0.0.2/tcp/27015";
acl = {
enableWhitelist = true;
whitelist = [
"@friend1"
"@friend2"
];
};
};
};
};
};

View File

@ -180,8 +180,8 @@ func (node *Node) Run() error {
serviceNet := svc.NewServiceNetwork(node.p2p, node.cfg, node.tunDev)
for name, addr := range node.cfg.Services {
proxy, err := svc.ProxyTo(addr)
for name, service := range node.cfg.Services {
proxy, err := svc.ProxyTo(service.Target)
if err != nil {
return err
}

View File

@ -30,12 +30,14 @@ type ServiceNetwork struct {
activeAddrs map[[16]byte]struct{}
activePorts map[[16]byte]map[uint16]struct{}
listeners map[[2]byte]Proxy
services map[[2]byte]config.Service
}
func (sn *ServiceNetwork) Register(serviceName string, proxy Proxy) {
svcId := config.MkServiceID(serviceName)
sn.listeners[svcId] = proxy
logger.With(zap.String("name", serviceName), zap.ByteString("id", svcId[:]), zap.String("description", proxy.Description)).Debug("Registered service")
sn.services[svcId] = sn.config.Services[serviceName]
logger.With(zap.String("name", serviceName), zap.String("id", fmt.Sprintf("%x", svcId[:])), zap.String("description", proxy.Description)).Info("Registered service")
}
func (sn *ServiceNetwork) EnsureListener(addr [16]byte, port uint16) bool {
@ -131,6 +133,7 @@ func NewServiceNetwork(host host.Host, cfg *config.Config, tunDev *hstun.TUN) Se
activeAddrs: make(map[[16]byte]struct{}),
activePorts: make(map[[16]byte]map[uint16]struct{}),
listeners: make(map[[2]byte]Proxy),
services: make(map[[2]byte]config.Service),
}
host.SetStreamHandler(Protocol, sn.streamHandler())

View File

@ -69,8 +69,9 @@ func ProxyTo(ma multiaddr.Multiaddr) (Proxy, error) {
type RemoteServiceProxyStatus byte
const (
RS_OK RemoteServiceProxyStatus = 0xf1
RS_NOT_SUPPORTED RemoteServiceProxyStatus = 0xf2
RS_OK RemoteServiceProxyStatus = 0xf1
RS_NOT_SUPPORTED RemoteServiceProxyStatus = 0xf2
RS_NOT_AUTHORIZED RemoteServiceProxyStatus = 0xf3
)
func RemoteServiceProxy(host host.Host, p peer.ID, svcId [2]byte) Proxy {
@ -95,7 +96,7 @@ func RemoteServiceProxy(host host.Host, p peer.ID, svcId [2]byte) Proxy {
logger.With(err).Error("Failed to read from stream")
return
} else if buf[0] != byte(RS_OK) {
logger.With(zap.String("peer", p.String()), zap.ByteString("service", svcId[:])).Warn("Peer does not support service")
logger.With(zap.String("peer", p.String()), zap.String("service", fmt.Sprintf("%x", svcId[:]))).Warn("Peer does not support service")
return
}
pipe(conn, stream)

View File

@ -5,8 +5,17 @@ import (
"github.com/hyprspace/hyprspace/config"
"github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/core/peer"
"go.uber.org/zap"
)
func (sn *ServiceNetwork) isRemoteBlocked(svcId [2]byte, remotePeer peer.ID) bool {
sv := sn.services[svcId]
_, isWhitelisted := sv.Whitelist[remotePeer]
_, isBlacklisted := sv.Blacklist[remotePeer]
return isBlacklisted || (sv.EnableWhitelist && !isWhitelisted)
}
func (sn *ServiceNetwork) streamHandler() func(network.Stream) {
return func(stream network.Stream) {
if _, ok := config.FindPeer(sn.config.Peers, stream.Conn().RemotePeer()); !ok {
@ -23,6 +32,17 @@ func (sn *ServiceNetwork) streamHandler() func(network.Stream) {
}
svcId := [2]byte(buf)
if proxy, ok := sn.listeners[svcId]; ok {
remotePeer := stream.Conn().RemotePeer()
if sn.isRemoteBlocked(svcId, remotePeer) {
logger.With(zap.String("service ID", fmt.Sprintf("%x", svcId[:]))).Debug("Connection from non-allowed peer")
_, err := stream.Write([]byte{byte(RS_NOT_AUTHORIZED)})
if err != nil {
logger.With(err).Error("Failed to send RS_NOT_AUTHORIZED")
return
}
return
}
_, err := stream.Write([]byte{byte(RS_OK)})
if err != nil {
logger.With(err).Error("Failed to write stream")