|
|
@ -12,43 +12,6 @@ You can start by reading the `ACL Configuration` section, then reading
|
|
|
|
exist to provide helpful context and advanced usage, but should not be
|
|
|
|
exist to provide helpful context and advanced usage, but should not be
|
|
|
|
necessary for a working setup.
|
|
|
|
necessary for a working setup.
|
|
|
|
|
|
|
|
|
|
|
|
# Glosary
|
|
|
|
|
|
|
|
## ACL
|
|
|
|
|
|
|
|
Access Control List:
|
|
|
|
|
|
|
|
This is your shared configuration for the network.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Subnet
|
|
|
|
|
|
|
|
In Wirenix, the word subnet represents any network of connected peers.
|
|
|
|
|
|
|
|
In the implementation, subnets are keyed by their `name` property. Subnet names
|
|
|
|
|
|
|
|
define the initial 32 bits after `fd` in of an the IPv6 addresses peers
|
|
|
|
|
|
|
|
connecting to the subnet will use. Generally speaking, one subnet = one
|
|
|
|
|
|
|
|
wireguard interface for each client on the subnet.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Peer
|
|
|
|
|
|
|
|
In Wirenix, peer is any machine with a unique public key In the
|
|
|
|
|
|
|
|
implementation, peer names define last 80 bits of their IPv6 address.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Group
|
|
|
|
|
|
|
|
In Wirenix, a group is just a tag that peers can have. These are used for
|
|
|
|
|
|
|
|
matching peers and can contain arbitrary names.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Endpoint
|
|
|
|
|
|
|
|
In wirenix, an endpoint specifies external IP of a peer that other peers should
|
|
|
|
|
|
|
|
connect to.
|
|
|
|
|
|
|
|
In the ACL configuration, endpoints can exist on subnets, groups, and peers,
|
|
|
|
|
|
|
|
but these are just for convenience. Think of adding an endpoint to a subnet or
|
|
|
|
|
|
|
|
group as being the same as adding the endpoint to all peers in the subnet or
|
|
|
|
|
|
|
|
group.
|
|
|
|
|
|
|
|
Endpoints have filters, which can specify for which connecting clients the
|
|
|
|
|
|
|
|
endpoint will apply to.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Filter
|
|
|
|
|
|
|
|
In Wirenix, a filter is used to select peers by their subnets, groups, and
|
|
|
|
|
|
|
|
names. A filter is made up of filter rules, specifying multiple rules will
|
|
|
|
|
|
|
|
yield the intersection of those rules.
|
|
|
|
|
|
|
|
Note that selecting by peer name will always return a list of 1 or 0 entries,
|
|
|
|
|
|
|
|
on account of names needing to be unique.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ACL Configuration
|
|
|
|
# ACL Configuration
|
|
|
|
The ACL is a nix attrset designed to be represented in JSON for easy importing
|
|
|
|
The ACL is a nix attrset designed to be represented in JSON for easy importing
|
|
|
|
and potential use outside of the nix ecosystem.
|
|
|
|
and potential use outside of the nix ecosystem.
|
|
|
@ -61,9 +24,10 @@ type ACL = {
|
|
|
|
groups: group[];
|
|
|
|
groups: group[];
|
|
|
|
peers: peer[];
|
|
|
|
peers: peer[];
|
|
|
|
connections: connection[];
|
|
|
|
connections: connection[];
|
|
|
|
extraArgs?: any;
|
|
|
|
extraArgs?: any; // goes to intermediate config
|
|
|
|
};
|
|
|
|
};
|
|
|
|
```
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Version is used to check for config compatibility and is recommended. Not
|
|
|
|
Version is used to check for config compatibility and is recommended. Not
|
|
|
|
specifying version will parse the configuration with the most recent parser
|
|
|
|
specifying version will parse the configuration with the most recent parser
|
|
|
|
available and generate a warning. Using an older configuration version than
|
|
|
|
available and generate a warning. Using an older configuration version than
|
|
|
@ -75,7 +39,7 @@ a version newer than any parsers available will throw an error.
|
|
|
|
type subnet = {
|
|
|
|
type subnet = {
|
|
|
|
name: str;
|
|
|
|
name: str;
|
|
|
|
endpoints?: endpoint[];
|
|
|
|
endpoints?: endpoint[];
|
|
|
|
extraArgs?: any;
|
|
|
|
extraArgs?: any; // goes to intermediate config subnet
|
|
|
|
};
|
|
|
|
};
|
|
|
|
```
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
@ -84,7 +48,7 @@ type subnet = {
|
|
|
|
type group = {
|
|
|
|
type group = {
|
|
|
|
name: str;
|
|
|
|
name: str;
|
|
|
|
endpoints?: endpoint[];
|
|
|
|
endpoints?: endpoint[];
|
|
|
|
extraArgs?: any;
|
|
|
|
extraArgs?: any; // goes to intermediate config group
|
|
|
|
};
|
|
|
|
};
|
|
|
|
```
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
@ -95,13 +59,13 @@ type peer = {
|
|
|
|
subnets: [subnetName: str]: {
|
|
|
|
subnets: [subnetName: str]: {
|
|
|
|
listenPort: int;
|
|
|
|
listenPort: int;
|
|
|
|
ipAddresses?: str[];
|
|
|
|
ipAddresses?: str[];
|
|
|
|
extraArgs?: any;
|
|
|
|
extraArgs?: any; // goes to intermediate config subnetConnection
|
|
|
|
};
|
|
|
|
};
|
|
|
|
publicKey: string;
|
|
|
|
publicKey: string;
|
|
|
|
privateKeyFile: string;
|
|
|
|
privateKeyFile: string;
|
|
|
|
groups?: str[];
|
|
|
|
groups?: str[];
|
|
|
|
endpoints?: endpoint[];
|
|
|
|
endpoints?: endpoint[];
|
|
|
|
extraArgs?: any;
|
|
|
|
extraArgs?: any; // goes to intermediate config peer
|
|
|
|
};
|
|
|
|
};
|
|
|
|
```
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
@ -111,7 +75,7 @@ type connection = {
|
|
|
|
a: filter;
|
|
|
|
a: filter;
|
|
|
|
b: filter;
|
|
|
|
b: filter;
|
|
|
|
subnets: str[];
|
|
|
|
subnets: str[];
|
|
|
|
extraArgs?: any;
|
|
|
|
extraArgs?: any; // merged into intermediate config peerConnection
|
|
|
|
};
|
|
|
|
};
|
|
|
|
```
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
@ -131,9 +95,10 @@ type endpoint = {
|
|
|
|
persistentKeepalive?: int;
|
|
|
|
persistentKeepalive?: int;
|
|
|
|
dynamicEndpointRefreshSeconds?: int;
|
|
|
|
dynamicEndpointRefreshSeconds?: int;
|
|
|
|
dynamicEndpointRefreshRestartSeconds?: int;
|
|
|
|
dynamicEndpointRefreshRestartSeconds?: int;
|
|
|
|
extraArgs?: any;
|
|
|
|
extraArgs?: any; // merged to intermediate config peerConnection.endpoin
|
|
|
|
};
|
|
|
|
};
|
|
|
|
```
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Endpoints are merged in this order: First lists of endpoints are merged top to
|
|
|
|
Endpoints are merged in this order: First lists of endpoints are merged top to
|
|
|
|
bottom, with the bottom endpoints overriding the top ones. Then, lists are
|
|
|
|
bottom, with the bottom endpoints overriding the top ones. Then, lists are
|
|
|
|
merged in this order: subnet -> group -> peer; with peer being the highest
|
|
|
|
merged in this order: subnet -> group -> peer; with peer being the highest
|
|
|
@ -163,6 +128,7 @@ WireNix consists of 4 main components:
|
|
|
|
2. Parser Modules
|
|
|
|
2. Parser Modules
|
|
|
|
3. The intermediate Configuration
|
|
|
|
3. The intermediate Configuration
|
|
|
|
4. Configuration Modules
|
|
|
|
4. Configuration Modules
|
|
|
|
|
|
|
|
|
|
|
|
The goal of splitting WireNix into modules is both for my own sanity when
|
|
|
|
The goal of splitting WireNix into modules is both for my own sanity when
|
|
|
|
developing, and to make it hackable without requiring users to make their own
|
|
|
|
developing, and to make it hackable without requiring users to make their own
|
|
|
|
fork. Users are able to specify their own Parser Modules, enabling them to use
|
|
|
|
fork. Users are able to specify their own Parser Modules, enabling them to use
|
|
|
@ -179,10 +145,7 @@ not need to consist only of NixOS peers (although at the moment, other peers
|
|
|
|
will have to be configured manually to conform to the expected settings). The
|
|
|
|
will have to be configured manually to conform to the expected settings). The
|
|
|
|
details of this file are documented in the `Top Level ACL` section.
|
|
|
|
details of this file are documented in the `Top Level ACL` section.
|
|
|
|
You can make your own ACL configuration format so long as you keep the
|
|
|
|
You can make your own ACL configuration format so long as you keep the
|
|
|
|
`version` field and set it to some unique name. You can then register your
|
|
|
|
`version` field and set it to some unique name.
|
|
|
|
parser which takes your ACL and produces an intermediate configuration like so:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
TODO
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Parser Modules
|
|
|
|
## Parser Modules
|
|
|
|
Parser Modules are responsible for taking an ACL and converting it to the
|
|
|
|
Parser Modules are responsible for taking an ACL and converting it to the
|
|
|
@ -191,7 +154,19 @@ ACL version field. A parser module must take an ACL and return the
|
|
|
|
corresponding Intermediate Configuration You can register your own parser
|
|
|
|
corresponding Intermediate Configuration You can register your own parser
|
|
|
|
module like so:
|
|
|
|
module like so:
|
|
|
|
|
|
|
|
|
|
|
|
TODO
|
|
|
|
```nix
|
|
|
|
|
|
|
|
modules.wirenix.additionalParsers = {
|
|
|
|
|
|
|
|
myParser = import ./my-parser.nix;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
and then in your acl, set the version:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
```nix
|
|
|
|
|
|
|
|
...
|
|
|
|
|
|
|
|
version = "myConfigurer";
|
|
|
|
|
|
|
|
...
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## Intermediate Configuration
|
|
|
|
## Intermediate Configuration
|
|
|
|
The Intermediate Configuration is a recursive attrset that is more suited for
|
|
|
|
The Intermediate Configuration is a recursive attrset that is more suited for
|
|
|
@ -216,6 +191,7 @@ type intermediateConfiguration = {
|
|
|
|
```
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### Peer
|
|
|
|
### Peer
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
```typescript
|
|
|
|
type peer = {
|
|
|
|
type peer = {
|
|
|
|
subnetConnections: {[subnetName: string]: subnetConnection};
|
|
|
|
subnetConnections: {[subnetName: string]: subnetConnection};
|
|
|
@ -228,6 +204,7 @@ type peer = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### Subnet
|
|
|
|
### Subnet
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
```typescript
|
|
|
|
type subnet = {
|
|
|
|
type subnet = {
|
|
|
|
peers: {[peerName: string]: peer};
|
|
|
|
peers: {[peerName: string]: peer};
|
|
|
@ -236,6 +213,7 @@ type subnet = {
|
|
|
|
```
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### Group
|
|
|
|
### Group
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
```typescript
|
|
|
|
type group = {
|
|
|
|
type group = {
|
|
|
|
peers: {[peerName: string]: peer};
|
|
|
|
peers: {[peerName: string]: peer};
|
|
|
@ -244,6 +222,7 @@ type group = {
|
|
|
|
```
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### Peer Connection
|
|
|
|
### Peer Connection
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
```typescript
|
|
|
|
type peerConnection = {
|
|
|
|
type peerConnection = {
|
|
|
|
peer: peer;
|
|
|
|
peer: peer;
|
|
|
@ -254,6 +233,7 @@ type peerConnection = {
|
|
|
|
```
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### Subnet Connection
|
|
|
|
### Subnet Connection
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
```typescript
|
|
|
|
type subnetConnection = {
|
|
|
|
type subnetConnection = {
|
|
|
|
subnet: subnet;
|
|
|
|
subnet: subnet;
|
|
|
@ -265,6 +245,7 @@ type subnetConnection = {
|
|
|
|
```
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### Endpoint
|
|
|
|
### Endpoint
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
```typescript
|
|
|
|
type endpoint = {
|
|
|
|
type endpoint = {
|
|
|
|
ip: str;
|
|
|
|
ip: str;
|
|
|
@ -289,21 +270,138 @@ intelligently selects which module to use (with priority being networkd >
|
|
|
|
network manager > static configuration). However, you can manually override
|
|
|
|
network manager > static configuration). However, you can manually override
|
|
|
|
which module is used (or use your own module) in your flake.nix file:
|
|
|
|
which module is used (or use your own module) in your flake.nix file:
|
|
|
|
|
|
|
|
|
|
|
|
TODO
|
|
|
|
```nix
|
|
|
|
|
|
|
|
modules.wirenix.configurer = "v0"; # there is no v0, this is just an example
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
or for your own module:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
```nix
|
|
|
|
|
|
|
|
modules.wirenix.additionalConfigurers.myConfigurer = import ./my-parser.nix;
|
|
|
|
|
|
|
|
modules.wirenix.configurer = "myConfigurer";
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Troubleshooting
|
|
|
|
|
|
|
|
Wirenix tries to stay seperated from the inner working of your config for as
|
|
|
|
|
|
|
|
long as possible. As a result, you can do most of your troubleshooting in the
|
|
|
|
|
|
|
|
nix repl:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
|
|
|
$ nix repl
|
|
|
|
|
|
|
|
$ nix-repl> :l <nixpkgs>
|
|
|
|
|
|
|
|
> Added 17766 variables.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$ nix-repl> :lf "sourcehut:~msalerno/wirenix"
|
|
|
|
|
|
|
|
> Added 11 variables.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$ nix-repl> parse = wirenix.lib.defaultParsers.v1 {inherit lib;}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$ nix-repl> configure = wirenix.lib.defaultConfigurers.static {inherit lib;}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$ nix-repl> acl = import ./examples/fullMesh/acl.nix # replace with your acl
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# get intermediate config
|
|
|
|
|
|
|
|
$ nix-repl> intConfig = parse acl
|
|
|
|
|
|
|
|
# you can explore the structure
|
|
|
|
|
|
|
|
$ nix-repl> intConfig
|
|
|
|
|
|
|
|
> { groups = { ... }; peers = { ... }; subnets = { ... }; }
|
|
|
|
|
|
|
|
# we can also see what the generated network config would be
|
|
|
|
|
|
|
|
$ nix-repl> genPeerConfig = configure intConfig
|
|
|
|
|
|
|
|
# `configure` is only partially applied, and genPeerConfig still needs a peer name
|
|
|
|
|
|
|
|
$ nix-repl> genPeerConfig
|
|
|
|
|
|
|
|
> «lambda @ /nix/store/h8gyjv62yddarvr533vi8f2rh5w0wh1p-source/configurers/static.nix:1:33»
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# we can then inspect the result
|
|
|
|
|
|
|
|
$ nix-repl> :p genPeerConfig "peer1"
|
|
|
|
|
|
|
|
> { networking = { wireguard = { interfaces = { ... }; }; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# printing the intermediate config with :p will cause a stack overflow
|
|
|
|
|
|
|
|
# but we have a helper function for this
|
|
|
|
|
|
|
|
$ nix-repl> :p wirenix.lib.breakIntermediateRecursion intConfig
|
|
|
|
|
|
|
|
> { a bunch of hard to read data }
|
|
|
|
|
|
|
|
# you can get a string and paste it into echo -e for pretty printing
|
|
|
|
|
|
|
|
$ nix-repl> lib.generators.toPretty {} (wirenix.lib.breakIntermediateRecursion intConfig)
|
|
|
|
|
|
|
|
> "even uglier result but it copy pastes well"
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
In your terminal:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
|
|
|
$ echo -e "paste the big text result from nix repl in here"
|
|
|
|
|
|
|
|
> a nice result
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
# Integrations:
|
|
|
|
# Integrations:
|
|
|
|
By default, WireNix supports setting wireguard keypairs with agenix-rekey if
|
|
|
|
By default, WireNix supports setting wireguard keypairs with agenix-rekey.
|
|
|
|
the module is detected (via the existence of `config.rekey`). WireNix also
|
|
|
|
WireNix also supports networkd, network manager, and the nixos static network
|
|
|
|
supports networkd, network manager, and the nixos static network configuration
|
|
|
|
configuration (default).
|
|
|
|
(in that order of preference).
|
|
|
|
|
|
|
|
|
|
|
|
Using networkd:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
```nix
|
|
|
|
|
|
|
|
TODO
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Using network manager:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
```nix
|
|
|
|
|
|
|
|
TODO
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Using static configuration:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
```nix
|
|
|
|
|
|
|
|
TODO
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Using agenix-rekey (assuming it's already set up properly)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
```nix
|
|
|
|
|
|
|
|
TODO
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
# Current Issues / Drawbacks
|
|
|
|
# Current Issues / Drawbacks
|
|
|
|
- To keep configuration and the initial POC simpler, assigning IP addresses
|
|
|
|
|
|
|
|
manually on subnets is not supported (yet).
|
|
|
|
|
|
|
|
- WireNix does not do NAT traversal, it's up to you to forward the correct
|
|
|
|
- WireNix does not do NAT traversal, it's up to you to forward the correct
|
|
|
|
ports on your NAT device(s) and apply the correct firewall rules on your
|
|
|
|
ports on your NAT device(s) and apply the correct firewall rules on your
|
|
|
|
router(s).
|
|
|
|
router(s).
|
|
|
|
- WireNix does not allow for dynamic addition of peers. If you need something
|
|
|
|
- WireNix does not allow for dynamic addition of peers. If you need something
|
|
|
|
more dynamic, look into Tailscale/Headscale.
|
|
|
|
more dynamic, look into Tailscale/Headscale.
|
|
|
|
- Peers cannot have multiple keys. If this is a desirable feature I may think
|
|
|
|
- Peers cannot have multiple keys. If this is a desirable feature I may think
|
|
|
|
of adding it, but I cannot think of a good reason for it.
|
|
|
|
of adding it, but I cannot think of a good reason for it.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Glosary
|
|
|
|
|
|
|
|
## ACL
|
|
|
|
|
|
|
|
Access Control List:
|
|
|
|
|
|
|
|
This is your shared configuration for the network.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Subnet
|
|
|
|
|
|
|
|
In Wirenix, the word subnet represents any network of connected peers.
|
|
|
|
|
|
|
|
In the implementation, subnets are keyed by their `name` property. Subnet names
|
|
|
|
|
|
|
|
define the initial 32 bits after `fd` in of an the IPv6 addresses peers
|
|
|
|
|
|
|
|
connecting to the subnet will use. Generally speaking, one subnet = one
|
|
|
|
|
|
|
|
wireguard interface for each client on the subnet.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Peer
|
|
|
|
|
|
|
|
In Wirenix, peer is any machine with a unique public key In the
|
|
|
|
|
|
|
|
implementation, peer names define last 80 bits of their IPv6 address.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Group
|
|
|
|
|
|
|
|
In Wirenix, a group is just a tag that peers can have. These are used for
|
|
|
|
|
|
|
|
matching peers and can contain arbitrary names.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Endpoint
|
|
|
|
|
|
|
|
In wirenix, an endpoint specifies external IP of a peer that other peers should
|
|
|
|
|
|
|
|
connect to.
|
|
|
|
|
|
|
|
In the ACL configuration, endpoints can exist on subnets, groups, and peers,
|
|
|
|
|
|
|
|
but these are just for convenience. Think of adding an endpoint to a subnet or
|
|
|
|
|
|
|
|
group as being the same as adding the endpoint to all peers in the subnet or
|
|
|
|
|
|
|
|
group.
|
|
|
|
|
|
|
|
Endpoints have filters, which can specify for which connecting clients the
|
|
|
|
|
|
|
|
endpoint will apply to.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Filter
|
|
|
|
|
|
|
|
In Wirenix, a filter is used to select peers by their subnets, groups, and
|
|
|
|
|
|
|
|
names. A filter is made up of filter rules, specifying multiple rules will
|
|
|
|
|
|
|
|
yield the intersection of those rules.
|
|
|
|
|
|
|
|
Note that selecting by peer name will always return a list of 1 or 0 entries,
|
|
|
|
|
|
|
|
on account of names needing to be unique.
|