diff --git a/config/config.go b/config/config.go index d9b289c..69fd8a5 100644 --- a/config/config.go +++ b/config/config.go @@ -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. diff --git a/dev/pkgs/go-jsonschema/default.nix b/dev/pkgs/go-jsonschema/default.nix index c6d1d15..ef7b19e 100644 --- a/dev/pkgs/go-jsonschema/default.nix +++ b/dev/pkgs/go-jsonschema/default.nix @@ -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" diff --git a/nixos/settings.nix b/nixos/settings.nix index e249e55..5d99112 100644 --- a/nixos/settings.nix +++ b/nixos/settings.nix @@ -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" + ]; + }; + }; }; }; }; diff --git a/node/node.go b/node/node.go index 2be97cd..adc1627 100644 --- a/node/node.go +++ b/node/node.go @@ -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 } diff --git a/svc/network.go b/svc/network.go index 0606bd7..5fbee1a 100644 --- a/svc/network.go +++ b/svc/network.go @@ -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()) diff --git a/svc/proxy.go b/svc/proxy.go index 07b1638..e30ef57 100644 --- a/svc/proxy.go +++ b/svc/proxy.go @@ -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) diff --git a/svc/receiver.go b/svc/receiver.go index ee45960..5c22c47 100644 --- a/svc/receiver.go +++ b/svc/receiver.go @@ -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")