add hostapd support
This commit is contained in:
parent
b7c46e01df
commit
f7b1c750aa
5 changed files with 335 additions and 13 deletions
|
@ -12,6 +12,7 @@
|
||||||
nixos-hardware.nixosModules.common-cpu-amd
|
nixos-hardware.nixosModules.common-cpu-amd
|
||||||
nixos-hardware.nixosModules.common-gpu-nvidia
|
nixos-hardware.nixosModules.common-gpu-nvidia
|
||||||
nixos-hardware.nixosModules.common-pc-hdd
|
nixos-hardware.nixosModules.common-pc-hdd
|
||||||
|
./services/hostapd.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
hardware.cpu.amd.updateMicrocode = true;
|
hardware.cpu.amd.updateMicrocode = true;
|
||||||
|
|
17
config/services/hostapd.nix
Normal file
17
config/services/hostapd.nix
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{ config, ... }: {
|
||||||
|
imports = [
|
||||||
|
../../modules/hostapd.nix
|
||||||
|
];
|
||||||
|
services.hostapd = {
|
||||||
|
countryCode = "DE";
|
||||||
|
interface = "wlp6s0";
|
||||||
|
ssid = "🦝";
|
||||||
|
wpa = true;
|
||||||
|
wpaPassphraseFile = config.sops.secrets."services/hostapd".path;
|
||||||
|
};
|
||||||
|
sops.secrets."services/hostapd" = {
|
||||||
|
restartUnits = [
|
||||||
|
"hostapd.service"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
251
modules/hostapd.nix
Normal file
251
modules/hostapd.nix
Normal file
|
@ -0,0 +1,251 @@
|
||||||
|
{ config, lib, pkgs, utils, ... }:
|
||||||
|
|
||||||
|
# TODO:
|
||||||
|
#
|
||||||
|
# asserts
|
||||||
|
# ensure that the nl80211 module is loaded/compiled in the kernel
|
||||||
|
# wpa_supplicant and hostapd on the same wireless interface doesn't make any sense
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
|
||||||
|
let
|
||||||
|
|
||||||
|
cfg = config.services.hostapd;
|
||||||
|
|
||||||
|
escapedInterface = utils.escapeSystemdPath cfg.interface;
|
||||||
|
|
||||||
|
configFile = pkgs.writeText "hostapd.conf" ''
|
||||||
|
interface=${cfg.interface}
|
||||||
|
driver=${cfg.driver}
|
||||||
|
ssid=${cfg.ssid}
|
||||||
|
hw_mode=${cfg.hwMode}
|
||||||
|
channel=${toString cfg.channel}
|
||||||
|
${optionalString (cfg.countryCode != null) "country_code=${cfg.countryCode}"}
|
||||||
|
${optionalString (cfg.countryCode != null) "ieee80211d=1"}
|
||||||
|
|
||||||
|
# logging (debug level)
|
||||||
|
logger_syslog=-1
|
||||||
|
logger_syslog_level=${toString cfg.logLevel}
|
||||||
|
logger_stdout=-1
|
||||||
|
logger_stdout_level=${toString cfg.logLevel}
|
||||||
|
|
||||||
|
ctrl_interface=/run/hostapd
|
||||||
|
ctrl_interface_group=${cfg.group}
|
||||||
|
|
||||||
|
${optionalString cfg.wpa ''
|
||||||
|
wpa=2
|
||||||
|
wpa_passphrase=${if cfg.wpaPassphrase then cfg.wpaPassphrase else "#WPA_PASSPHRASE#"}
|
||||||
|
''}
|
||||||
|
${optionalString cfg.noScan "noscan=1"}
|
||||||
|
|
||||||
|
${cfg.extraConfig}
|
||||||
|
'';
|
||||||
|
|
||||||
|
in
|
||||||
|
|
||||||
|
{
|
||||||
|
###### interface
|
||||||
|
|
||||||
|
options = {
|
||||||
|
|
||||||
|
services.hostapd = {
|
||||||
|
|
||||||
|
enable = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Enable putting a wireless interface into infrastructure mode,
|
||||||
|
allowing other wireless devices to associate with the wireless
|
||||||
|
interface and do wireless networking. A simple access point will
|
||||||
|
<option>enable hostapd.wpa</option>,
|
||||||
|
<option>hostapd.wpaPassphrase</option>, and
|
||||||
|
<option>hostapd.ssid</option>, as well as DHCP on the wireless
|
||||||
|
interface to provide IP addresses to the associated stations, and
|
||||||
|
NAT (from the wireless interface to an upstream interface).
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
interface = mkOption {
|
||||||
|
default = "";
|
||||||
|
example = "wlp2s0";
|
||||||
|
type = types.str;
|
||||||
|
description = ''
|
||||||
|
The interfaces <command>hostapd</command> will use.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
noScan = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Do not scan for overlapping BSSs in HT40+/- mode.
|
||||||
|
Caution: turning this on will violate regulatory requirements!
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
driver = mkOption {
|
||||||
|
default = "nl80211";
|
||||||
|
example = "hostapd";
|
||||||
|
type = types.str;
|
||||||
|
description = ''
|
||||||
|
Which driver <command>hostapd</command> will use.
|
||||||
|
Most applications will probably use the default.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
ssid = mkOption {
|
||||||
|
default = "nixos";
|
||||||
|
example = "mySpecialSSID";
|
||||||
|
type = types.str;
|
||||||
|
description = "SSID to be used in IEEE 802.11 management frames.";
|
||||||
|
};
|
||||||
|
|
||||||
|
hwMode = mkOption {
|
||||||
|
default = "g";
|
||||||
|
type = types.enum [ "a" "b" "g" ];
|
||||||
|
description = ''
|
||||||
|
Operation mode.
|
||||||
|
(a = IEEE 802.11a, b = IEEE 802.11b, g = IEEE 802.11g).
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
channel = mkOption {
|
||||||
|
default = 7;
|
||||||
|
example = 11;
|
||||||
|
type = types.int;
|
||||||
|
description = ''
|
||||||
|
Channel number (IEEE 802.11)
|
||||||
|
Please note that some drivers do not use this value from
|
||||||
|
<command>hostapd</command> and the channel will need to be configured
|
||||||
|
separately with <command>iwconfig</command>.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
group = mkOption {
|
||||||
|
default = "wheel";
|
||||||
|
example = "network";
|
||||||
|
type = types.str;
|
||||||
|
description = ''
|
||||||
|
Members of this group can control <command>hostapd</command>.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
wpa = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = ''
|
||||||
|
Enable WPA (IEEE 802.11i/D3.0) to authenticate with the access point.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
wpaPassphrase = mkOption {
|
||||||
|
default = null;
|
||||||
|
example = "any_64_char_string";
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
description = ''
|
||||||
|
WPA-PSK (pre-shared-key) passphrase. Clients will need this
|
||||||
|
passphrase to associate with this access point.
|
||||||
|
Warning: This passphrase will get put into a world-readable file in
|
||||||
|
the Nix store!
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
wpaPassphraseFile = mkOption {
|
||||||
|
default = null;
|
||||||
|
example = "/run/secrets/wpa_passphrase";
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
description = ''
|
||||||
|
File containing WPA-PSK passphrase. Clients will need this
|
||||||
|
passphrase to associate with this access point.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
logLevel = mkOption {
|
||||||
|
default = 2;
|
||||||
|
type = types.int;
|
||||||
|
description = ''
|
||||||
|
Levels (minimum value for logged events):
|
||||||
|
0 = verbose debugging
|
||||||
|
1 = debugging
|
||||||
|
2 = informational messages
|
||||||
|
3 = notification
|
||||||
|
4 = warning
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
countryCode = mkOption {
|
||||||
|
default = null;
|
||||||
|
example = "US";
|
||||||
|
type = with types; nullOr str;
|
||||||
|
description = ''
|
||||||
|
Country code (ISO/IEC 3166-1). Used to set regulatory domain.
|
||||||
|
Set as needed to indicate country in which device is operating.
|
||||||
|
This can limit available channels and transmit power.
|
||||||
|
These two octets are used as the first two octets of the Country String
|
||||||
|
(dot11CountryString).
|
||||||
|
If set this enables IEEE 802.11d. This advertises the countryCode and
|
||||||
|
the set of allowed channels and transmit power levels based on the
|
||||||
|
regulatory limits.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
extraConfig = mkOption {
|
||||||
|
default = "";
|
||||||
|
example = ''
|
||||||
|
auth_algo=0
|
||||||
|
ieee80211n=1
|
||||||
|
ht_capab=[HT40-][SHORT-GI-40][DSSS_CCK-40]
|
||||||
|
'';
|
||||||
|
type = types.lines;
|
||||||
|
description = "Extra configuration options to put in hostapd.conf.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
disabledModules = [ "services/networking/hostapd.nix" ];
|
||||||
|
|
||||||
|
###### implementation
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
|
||||||
|
assertions = [
|
||||||
|
{
|
||||||
|
assertion = cfg.wpa != null -> (cfg.wpaPassphrase != null || cfg.wpaPassphraseFile != null);
|
||||||
|
message = "Either wpaPassphrase or wpaPassphraseFile must be set if wpa is enabled.";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = cfg.wpaPassphraseFile != null -> cfg.wpaPassphrase == null;
|
||||||
|
message = "You cannot provide a wpaPassphrase and a wpaPassphraseFile!";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
environment.systemPackages = [ pkgs.hostapd ];
|
||||||
|
|
||||||
|
services.udev.packages = optional (cfg.countryCode != null) [ pkgs.crda ];
|
||||||
|
|
||||||
|
systemd.services.hostapd =
|
||||||
|
{
|
||||||
|
description = "hostapd wireless AP";
|
||||||
|
|
||||||
|
path = [ pkgs.hostapd ];
|
||||||
|
after = [ "sys-subsystem-net-devices-${escapedInterface}.device" ];
|
||||||
|
bindsTo = [ "sys-subsystem-net-devices-${escapedInterface}.device" ];
|
||||||
|
requiredBy = [ "network-link-${cfg.interface}.service" ];
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
|
||||||
|
preStart = mkIf cfg.wpaPassphraseFile != null ''
|
||||||
|
PASSPHRASE=$(cat ${cfg.wpaPassphraseFile})
|
||||||
|
sed 's|#WPA_PASSPHRASE#|$PASSPHRASE|g' ${configFile} > /run/hostapd/hostapd.conf
|
||||||
|
'';
|
||||||
|
|
||||||
|
serviceConfig =
|
||||||
|
{
|
||||||
|
ExecStart = "${pkgs.hostapd}/bin/hostapd ${if cfg.wpaPassphraseFile != null then "/run/hostapd/hostapd.conf" else configFile}";
|
||||||
|
Restart = "always";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
systemd.tmpfiles.rules = mkIf cfg.wpaPassphraseFile != null [
|
||||||
|
"d '/run/hostapd' 0700 root root - -"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ services:
|
||||||
gitea_token: ENC[AES256_GCM,data:v0Ej8841I1F/dK5ZplRzZlvngpueMQKspM5USzX9VkOEmpCs2NA3+Q==,iv:fZisAuyqk7ATFx6qHYkScUeS8SsikjiPzVovZjGnUYM=,tag:7+O+Sn7unPDy88a6T70Jmg==,type:str]
|
gitea_token: ENC[AES256_GCM,data:v0Ej8841I1F/dK5ZplRzZlvngpueMQKspM5USzX9VkOEmpCs2NA3+Q==,iv:fZisAuyqk7ATFx6qHYkScUeS8SsikjiPzVovZjGnUYM=,tag:7+O+Sn7unPDy88a6T70Jmg==,type:str]
|
||||||
github_token: ENC[AES256_GCM,data:AWMeX+P8YHGpSuH+5KqvE9zNxkEPKGvdRaQjNysO4/XE4csGjCvmjA==,iv:MCRtws/SM7lWS2/2pp5tbeX7+I5h4LVd9bJp//ln9hs=,tag:LMEGWFAaOqH0fqfNgc87AQ==,type:str]
|
github_token: ENC[AES256_GCM,data:AWMeX+P8YHGpSuH+5KqvE9zNxkEPKGvdRaQjNysO4/XE4csGjCvmjA==,iv:MCRtws/SM7lWS2/2pp5tbeX7+I5h4LVd9bJp//ln9hs=,tag:LMEGWFAaOqH0fqfNgc87AQ==,type:str]
|
||||||
aws_credentials: ENC[AES256_GCM,data:yxJU6d6BMi+LHUPimMkgr5h6accGXQXxFu9A0swdwKII/Xfo4ALAw4J4aEhpnNuK8JwmzuuDdTDGnilzuEATeaANa2cNXps6AWw8Hem8idw585xTcU1YBEOdbBSs/mKK6S+Da1OU5jC1atrCCWY7cg==,iv:tAEGsniZ7N/jBp7btLlD1pNcF4NvEmpO6zXji1H29t8=,tag:lmAB3QMfaT3ljDmr+8IBHA==,type:str]
|
aws_credentials: ENC[AES256_GCM,data:yxJU6d6BMi+LHUPimMkgr5h6accGXQXxFu9A0swdwKII/Xfo4ALAw4J4aEhpnNuK8JwmzuuDdTDGnilzuEATeaANa2cNXps6AWw8Hem8idw585xTcU1YBEOdbBSs/mKK6S+Da1OU5jC1atrCCWY7cg==,iv:tAEGsniZ7N/jBp7btLlD1pNcF4NvEmpO6zXji1H29t8=,tag:lmAB3QMfaT3ljDmr+8IBHA==,type:str]
|
||||||
|
hostapd: ENC[AES256_GCM,data:XDLRbra/RadQE/t6cPG94A==,iv:EafhBPWviYaZLodGcIDTfxUoLnnaLQKyWLeSwZe9d0Q=,tag:yvfqt74qR/en/HoW2HmRKQ==,type:int]
|
||||||
security:
|
security:
|
||||||
restic:
|
restic:
|
||||||
password: ENC[AES256_GCM,data:n+M6pfe0YrONaYo3HSnijHxhThg=,iv:0J2t+58tYRJD1GmnJa8w30U+RwOl67eWeHhvLk0eeks=,tag:ivuZqpGrU7ZHFZ4IiMvxBw==,type:str]
|
password: ENC[AES256_GCM,data:n+M6pfe0YrONaYo3HSnijHxhThg=,iv:0J2t+58tYRJD1GmnJa8w30U+RwOl67eWeHhvLk0eeks=,tag:ivuZqpGrU7ZHFZ4IiMvxBw==,type:str]
|
||||||
|
@ -36,8 +37,8 @@ sops:
|
||||||
WnV3QWxtalIzWFdoQmpDTmJsNGdNOW8K++rFGXy0G6Gcu2gQwSP6xfXInQ/y5nh5
|
WnV3QWxtalIzWFdoQmpDTmJsNGdNOW8K++rFGXy0G6Gcu2gQwSP6xfXInQ/y5nh5
|
||||||
2oGp8sfOLFWnNI4SWL0ChP47K3C/9ysUHwQnUYPbRafZ/4X6cN40ZQ==
|
2oGp8sfOLFWnNI4SWL0ChP47K3C/9ysUHwQnUYPbRafZ/4X6cN40ZQ==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
lastmodified: "2022-04-24T10:49:24Z"
|
lastmodified: "2022-04-24T20:20:10Z"
|
||||||
mac: ENC[AES256_GCM,data:gSBsJnw13ic3ysf+FevyXi3zhyfld5aChTRa49OE65FcedbqqToctALLUo4UXZbbPonu3ixBV4R297NDjWFlS+3QFR15uXMZpbHcVSLPtDF+TTx4c90MA+V2qrfTqPGaBb8zBqGOHqIOYBNEQWfjirYALnguvTBFiYUoGE9nswE=,iv:NYc+4/SLz92dYP+zdwW3WTpB5wyeTQ2SfpWLWP16HoY=,tag:qJ0AF8RtPpwJ+dNPlo+jNA==,type:str]
|
mac: ENC[AES256_GCM,data:qANlJ5sFKi6OE6ZaI0mUYHkb1UvJN9cxIlZ213dN9vtOa3eaNAHqW8SHa3Gt1lSjgoY9ux0SoaDb4Pyevq2COVm2dHlHPtfhCzvpVpjy3xLI7Q69OFLRgKxUQj2ZEtXWKy7ea82EVSTEWavlDaXzd/lpaiKugYgM/xwQjzbifgQ=,iv:Oc+SPuQKA1awYDZwdQgHWbVGJFRbTcWPveNNlM3b7y0=,tag:Z4s44hHbdL4mwFaKPq9lmQ==,type:str]
|
||||||
pgp:
|
pgp:
|
||||||
- created_at: "2022-04-24T10:34:20Z"
|
- created_at: "2022-04-24T10:34:20Z"
|
||||||
enc: |
|
enc: |
|
||||||
|
|
|
@ -8,7 +8,7 @@ in
|
||||||
SOA = {
|
SOA = {
|
||||||
nameServer = "ns1.chir.rs.";
|
nameServer = "ns1.chir.rs.";
|
||||||
adminEmail = "lotte@chir.rs";
|
adminEmail = "lotte@chir.rs";
|
||||||
serial = 9;
|
serial = 10;
|
||||||
};
|
};
|
||||||
NS = [
|
NS = [
|
||||||
"ns1.chir.rs."
|
"ns1.chir.rs."
|
||||||
|
@ -38,14 +38,6 @@ in
|
||||||
(ttl zoneTTL (aaaa "fd00:e621:e621::1"))
|
(ttl zoneTTL (aaaa "fd00:e621:e621::1"))
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
nas = {
|
|
||||||
A = [
|
|
||||||
(ttl zoneTTL (a "10.0.2.2"))
|
|
||||||
];
|
|
||||||
AAAA = [
|
|
||||||
(ttl zoneTTL (aaaa "fd00:e621:e621:2::2"))
|
|
||||||
];
|
|
||||||
};
|
|
||||||
nixos-8gb-fsn1-1 = {
|
nixos-8gb-fsn1-1 = {
|
||||||
AAAA = [
|
AAAA = [
|
||||||
(ttl zoneTTL (aaaa "fd0d:a262:1fa6:e621:b4e1:8ff:e658:6f49"))
|
(ttl zoneTTL (aaaa "fd0d:a262:1fa6:e621:b4e1:8ff:e658:6f49"))
|
||||||
|
@ -187,13 +179,73 @@ in
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
nas = {
|
||||||
|
AAAA = [
|
||||||
|
(ttl zoneTTL (aaaa "fd0d:a262:1fa6:e621:bc9b:6a33:86e4:873b"))
|
||||||
|
];
|
||||||
|
SSHFP = [
|
||||||
|
{
|
||||||
|
algorithm = "rsa";
|
||||||
|
mode = "sha1";
|
||||||
|
fingerprint = "13e1173d96b822c98a7b3cd47be2e830f7758671";
|
||||||
|
ttl = zoneTTL;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
algorithm = "rsa";
|
||||||
|
mode = "sha256";
|
||||||
|
fingerprint = "2e87a3fd00918e4f1e47d3b14b59e846ee016a0d3269cb2524c8d28b121e130e";
|
||||||
|
ttl = zoneTTL;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
algorithm = "ed25519";
|
||||||
|
mode = "sha1";
|
||||||
|
fingerprint = "d1df2d244980a5e4dde37eed678b59a2239ca2ac";
|
||||||
|
ttl = zoneTTL;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
algorithm = "ed25519";
|
||||||
|
mode = "sha256";
|
||||||
|
fingerprint = "33d6c993ee3789fb6a2e60c243da7095eb79ce8e522b087f8a31ea400d7b034e";
|
||||||
|
ttl = zoneTTL;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
# TODO: add TLSA
|
||||||
|
HTTPS = [
|
||||||
|
{
|
||||||
|
svcPriority = 1;
|
||||||
|
targetName = ".";
|
||||||
|
alpn = [ "http/1.1" "h2" "h3" ];
|
||||||
|
ipv6hint = [ "fd0d:a262:1fa6:e621:bc9b:6a33:86e4:873b" ];
|
||||||
|
ttl = zoneTTL;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
CAA = [
|
||||||
|
{
|
||||||
|
issuerCritical = false;
|
||||||
|
tag = "issue";
|
||||||
|
value = "letsencrypt.org";
|
||||||
|
ttl = zoneTTL;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
issuerCritical = false;
|
||||||
|
tag = "issuewild";
|
||||||
|
value = "letsencrypt.org";
|
||||||
|
ttl = zoneTTL;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
issuerCritical = false;
|
||||||
|
tag = "iodef";
|
||||||
|
value = "mailto:lotte@chir.rs";
|
||||||
|
ttl = zoneTTL;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
grafana.CNAME = [ "nixos-8gb-fsn1-1" ];
|
grafana.CNAME = [ "nixos-8gb-fsn1-1" ];
|
||||||
minio.CNAME = [ "nixos-8gb-fsn1-1" ];
|
minio.CNAME = [ "nixos-8gb-fsn1-1" ];
|
||||||
minio-console.CNAME = [ "nixos-8gb-fsn1-1" ];
|
minio-console.CNAME = [ "nixos-8gb-fsn1-1" ];
|
||||||
backup.CNAME = [ "nas" ];
|
backup.CNAME = [ "nas" ];
|
||||||
cache.CNAME = [ "nutty-noon" ];
|
hydra.CNAME = [ "nas" ];
|
||||||
hydra.CNAME = [ "nutty-noon" ];
|
|
||||||
_acme-challenge = delegateTo [
|
_acme-challenge = delegateTo [
|
||||||
"ns1.chir.rs."
|
"ns1.chir.rs."
|
||||||
"ns2.chir.rs."
|
"ns2.chir.rs."
|
||||||
|
|
Loading…
Reference in a new issue