Update hysteria2 realm
This commit is contained in:
parent
34c90e6f2e
commit
2e1a7a53ae
11 changed files with 109 additions and 43 deletions
|
|
@ -42,16 +42,16 @@ type DNSQueryOptions struct {
|
||||||
ClientSubnet netip.Prefix
|
ClientSubnet netip.Prefix
|
||||||
}
|
}
|
||||||
|
|
||||||
func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (*DNSQueryOptions, error) {
|
func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (DNSQueryOptions, error) {
|
||||||
if options == nil {
|
if options == nil || options.Server == "" {
|
||||||
return &DNSQueryOptions{}, nil
|
return DNSQueryOptions{}, nil
|
||||||
}
|
}
|
||||||
transportManager := service.FromContext[DNSTransportManager](ctx)
|
transportManager := service.FromContext[DNSTransportManager](ctx)
|
||||||
transport, loaded := transportManager.Transport(options.Server)
|
transport, loaded := transportManager.Transport(options.Server)
|
||||||
if !loaded {
|
if !loaded {
|
||||||
return nil, E.New("domain resolver not found: " + options.Server)
|
return DNSQueryOptions{}, E.New("domain resolver not found: " + options.Server)
|
||||||
}
|
}
|
||||||
return &DNSQueryOptions{
|
return DNSQueryOptions{
|
||||||
Transport: transport,
|
Transport: transport,
|
||||||
Strategy: C.DomainStrategy(options.Strategy),
|
Strategy: C.DomainStrategy(options.Strategy),
|
||||||
DisableCache: options.DisableCache,
|
DisableCache: options.DisableCache,
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ icon: material/alert-decagram
|
||||||
"token": "",
|
"token": "",
|
||||||
"realm_id": "",
|
"realm_id": "",
|
||||||
"stun_servers": [],
|
"stun_servers": [],
|
||||||
|
"stun_domain_resolver": "", // or {}
|
||||||
"http_client": {}
|
"http_client": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -209,7 +210,15 @@ Outbounds must use the same `realm_id` to find this server.
|
||||||
|
|
||||||
List of STUN servers (`host` or `host:port`) used to discover public addresses.
|
List of STUN servers (`host` or `host:port`) used to discover public addresses.
|
||||||
|
|
||||||
Port defaults to `3478`.
|
#### realm.stun_domain_resolver
|
||||||
|
|
||||||
|
Set domain resolver to use for resolving STUN server domain names.
|
||||||
|
|
||||||
|
This option uses the same format as the [route DNS rule action](/configuration/dns/rule_action/#route) without the `action` field.
|
||||||
|
|
||||||
|
Setting this option directly to a string is equivalent to setting `server` of this options.
|
||||||
|
|
||||||
|
If empty, the default domain resolver is used.
|
||||||
|
|
||||||
#### realm.http_client
|
#### realm.http_client
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ icon: material/alert-decagram
|
||||||
"token": "",
|
"token": "",
|
||||||
"realm_id": "",
|
"realm_id": "",
|
||||||
"stun_servers": [],
|
"stun_servers": [],
|
||||||
|
"stun_domain_resolver": "", // 或 {}
|
||||||
"http_client": {}
|
"http_client": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -206,7 +207,15 @@ Realm 上的槽位标识符。
|
||||||
|
|
||||||
用于发现公网地址的 STUN 服务器列表(`host` 或 `host:port`)。
|
用于发现公网地址的 STUN 服务器列表(`host` 或 `host:port`)。
|
||||||
|
|
||||||
端口默认为 `3478`。
|
#### realm.stun_domain_resolver
|
||||||
|
|
||||||
|
用于解析 STUN 服务器域名的域名解析器。
|
||||||
|
|
||||||
|
此选项的格式与 [路由 DNS 规则动作](/zh/configuration/dns/rule_action/#route) 相同,但不包含 `action` 字段。
|
||||||
|
|
||||||
|
若直接将此选项设置为字符串,则等同于设置该选项的 `server` 字段。
|
||||||
|
|
||||||
|
如果为空,则使用默认域名解析器。
|
||||||
|
|
||||||
#### realm.http_client
|
#### realm.http_client
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -191,7 +191,7 @@ The same slot identifier the target Hysteria2 server registered.
|
||||||
|
|
||||||
List of STUN servers (`host` or `host:port`) used to discover this client's public addresses.
|
List of STUN servers (`host` or `host:port`) used to discover this client's public addresses.
|
||||||
|
|
||||||
Port defaults to `3478`.
|
Domain names are resolved using [`domain_resolver`](/configuration/shared/dial/#domain_resolver) from Dial Fields.
|
||||||
|
|
||||||
#### realm.http_client
|
#### realm.http_client
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -189,7 +189,7 @@ Realm 的 Bearer 令牌,需与 realm 上配置的 `users[].token` 之一匹配
|
||||||
|
|
||||||
用于发现本客户端公网地址的 STUN 服务器列表(`host` 或 `host:port`)。
|
用于发现本客户端公网地址的 STUN 服务器列表(`host` 或 `host:port`)。
|
||||||
|
|
||||||
端口默认为 `3478`。
|
域名通过 [拨号字段](/zh/configuration/shared/dial/) 中的 [`domain_resolver`](/zh/configuration/shared/dial/#domain_resolver) 解析。
|
||||||
|
|
||||||
#### realm.http_client
|
#### realm.http_client
|
||||||
|
|
||||||
|
|
|
||||||
2
go.mod
2
go.mod
|
|
@ -40,7 +40,7 @@ require (
|
||||||
github.com/sagernet/sing v0.8.10-0.20260428084616-2bc976d03e39
|
github.com/sagernet/sing v0.8.10-0.20260428084616-2bc976d03e39
|
||||||
github.com/sagernet/sing-cloudflared v0.1.0
|
github.com/sagernet/sing-cloudflared v0.1.0
|
||||||
github.com/sagernet/sing-mux v0.3.4
|
github.com/sagernet/sing-mux v0.3.4
|
||||||
github.com/sagernet/sing-quic v0.6.2-0.20260510042401-4055a2bb2381
|
github.com/sagernet/sing-quic v0.6.2-0.20260511084842-9deddf006024
|
||||||
github.com/sagernet/sing-shadowsocks v0.2.8
|
github.com/sagernet/sing-shadowsocks v0.2.8
|
||||||
github.com/sagernet/sing-shadowsocks2 v0.2.1
|
github.com/sagernet/sing-shadowsocks2 v0.2.1
|
||||||
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11
|
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11
|
||||||
|
|
|
||||||
4
go.sum
4
go.sum
|
|
@ -248,8 +248,8 @@ github.com/sagernet/sing-cloudflared v0.1.0 h1:to+2fcCx8zu4X/DirRw9Ihc+FrEZ7oEyI
|
||||||
github.com/sagernet/sing-cloudflared v0.1.0/go.mod h1:bH2NKX+NpDTY1Zkxfboxw6MXB/ZywaNLmrDJYgKMJ2Y=
|
github.com/sagernet/sing-cloudflared v0.1.0/go.mod h1:bH2NKX+NpDTY1Zkxfboxw6MXB/ZywaNLmrDJYgKMJ2Y=
|
||||||
github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s=
|
github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s=
|
||||||
github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk=
|
github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk=
|
||||||
github.com/sagernet/sing-quic v0.6.2-0.20260510042401-4055a2bb2381 h1:bwja5I7Sgr4Z1nyCLlN6T5eBhhSE/ilYZmkEFIoPAhQ=
|
github.com/sagernet/sing-quic v0.6.2-0.20260511084842-9deddf006024 h1:P1iab6udg2I2igIrn+mNKpPZNcuejSqno3jwJ/94upw=
|
||||||
github.com/sagernet/sing-quic v0.6.2-0.20260510042401-4055a2bb2381/go.mod h1:+oqD54aHel4ALKkp1hVXWCgLU/EjLojvm6AUzDfvj0I=
|
github.com/sagernet/sing-quic v0.6.2-0.20260511084842-9deddf006024/go.mod h1:+oqD54aHel4ALKkp1hVXWCgLU/EjLojvm6AUzDfvj0I=
|
||||||
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
|
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
|
||||||
github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI=
|
github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI=
|
||||||
github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo=
|
github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo=
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,10 @@ type Hysteria2InboundOptions struct {
|
||||||
IgnoreClientBandwidth bool `json:"ignore_client_bandwidth,omitempty"`
|
IgnoreClientBandwidth bool `json:"ignore_client_bandwidth,omitempty"`
|
||||||
InboundTLSOptionsContainer
|
InboundTLSOptionsContainer
|
||||||
QUICOptions
|
QUICOptions
|
||||||
Masquerade *Hysteria2Masquerade `json:"masquerade,omitempty"`
|
Masquerade *Hysteria2Masquerade `json:"masquerade,omitempty"`
|
||||||
BBRProfile string `json:"bbr_profile,omitempty"`
|
BBRProfile string `json:"bbr_profile,omitempty"`
|
||||||
BrutalDebug bool `json:"brutal_debug,omitempty"`
|
BrutalDebug bool `json:"brutal_debug,omitempty"`
|
||||||
Realm *Hysteria2Realm `json:"realm,omitempty"`
|
Realm *Hysteria2InboundRealm `json:"realm,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Hysteria2Realm struct {
|
type Hysteria2Realm struct {
|
||||||
|
|
@ -33,6 +33,11 @@ type Hysteria2Realm struct {
|
||||||
HTTPClient *HTTPClientOptions `json:"http_client,omitempty"`
|
HTTPClient *HTTPClientOptions `json:"http_client,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Hysteria2InboundRealm struct {
|
||||||
|
Hysteria2Realm
|
||||||
|
STUNDomainResolver *DomainResolveOptions `json:"stun_domain_resolver,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type Hysteria2Obfs struct {
|
type Hysteria2Obfs struct {
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
Password string `json:"password,omitempty"`
|
Password string `json:"password,omitempty"`
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -18,11 +19,13 @@ import (
|
||||||
qtls "github.com/sagernet/sing-quic"
|
qtls "github.com/sagernet/sing-quic"
|
||||||
"github.com/sagernet/sing-quic/hysteria"
|
"github.com/sagernet/sing-quic/hysteria"
|
||||||
"github.com/sagernet/sing-quic/hysteria2"
|
"github.com/sagernet/sing-quic/hysteria2"
|
||||||
|
"github.com/sagernet/sing-quic/hysteria2/realm"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
"github.com/sagernet/sing/common/auth"
|
"github.com/sagernet/sing/common/auth"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RegisterInbound(registry *inbound.Registry) {
|
func RegisterInbound(registry *inbound.Registry) {
|
||||||
|
|
@ -114,9 +117,35 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
||||||
} else {
|
} else {
|
||||||
udpTimeout = C.UDPTimeout
|
udpTimeout = C.UDPTimeout
|
||||||
}
|
}
|
||||||
realmOptions, err := buildRealmOptions(ctx, logger, options.Realm)
|
var realmOptions *realm.Options
|
||||||
if err != nil {
|
if options.Realm != nil {
|
||||||
return nil, err
|
queryOptions, err := adapter.DNSQueryOptionsFrom(ctx, options.Realm.STUNDomainResolver)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
httpClientTransport, err := service.FromContext[adapter.HTTPClientManager](ctx).ResolveTransport(ctx, logger, common.PtrValueOrDefault(options.Realm.HTTPClient))
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "create realm http client")
|
||||||
|
}
|
||||||
|
dnsRouter := service.FromContext[adapter.DNSRouter](ctx)
|
||||||
|
realmOptions = &realm.Options{
|
||||||
|
ServerURL: options.Realm.ServerURL,
|
||||||
|
Token: options.Realm.Token,
|
||||||
|
RealmID: options.Realm.RealmID,
|
||||||
|
STUNServers: options.Realm.STUNServers,
|
||||||
|
HTTPClient: &http.Client{Transport: httpClientTransport},
|
||||||
|
Resolver: func(ctx context.Context, host string, ipv4, ipv6 bool) ([]netip.Addr, error) {
|
||||||
|
dnsOptions := queryOptions
|
||||||
|
switch {
|
||||||
|
case ipv4 && !ipv6:
|
||||||
|
dnsOptions.Strategy = C.DomainStrategyIPv4Only
|
||||||
|
case !ipv4 && ipv6:
|
||||||
|
dnsOptions.Strategy = C.DomainStrategyIPv6Only
|
||||||
|
}
|
||||||
|
return dnsRouter.Lookup(ctx, host, dnsOptions)
|
||||||
|
},
|
||||||
|
Logger: logger,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
hysteriaService, err := hysteria2.NewService[int](hysteria2.ServiceOptions{
|
hysteriaService, err := hysteria2.NewService[int](hysteria2.ServiceOptions{
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ package hysteria2
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -18,12 +20,14 @@ import (
|
||||||
qtls "github.com/sagernet/sing-quic"
|
qtls "github.com/sagernet/sing-quic"
|
||||||
"github.com/sagernet/sing-quic/hysteria"
|
"github.com/sagernet/sing-quic/hysteria"
|
||||||
"github.com/sagernet/sing-quic/hysteria2"
|
"github.com/sagernet/sing-quic/hysteria2"
|
||||||
|
"github.com/sagernet/sing-quic/hysteria2/realm"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
"github.com/sagernet/sing/common/bufio"
|
"github.com/sagernet/sing/common/bufio"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
"github.com/sagernet/sing/common/logger"
|
"github.com/sagernet/sing/common/logger"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RegisterOutbound(registry *outbound.Registry) {
|
func RegisterOutbound(registry *outbound.Registry) {
|
||||||
|
|
@ -66,13 +70,43 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
||||||
return nil, E.New("unknown obfs type: ", options.Obfs.Type)
|
return nil, E.New("unknown obfs type: ", options.Obfs.Type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain())
|
outboundDialer, err := dialer.NewWithOptions(dialer.Options{
|
||||||
|
Context: ctx,
|
||||||
|
Options: options.DialerOptions,
|
||||||
|
RemoteIsDomain: options.ServerIsDomain(),
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
realmOptions, err := buildRealmOptions(ctx, logger, options.Realm)
|
var realmOptions *realm.Options
|
||||||
if err != nil {
|
if options.Realm != nil {
|
||||||
return nil, err
|
queryOptions, err := adapter.DNSQueryOptionsFrom(ctx, options.DialerOptions.DomainResolver)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
httpClientTransport, err := service.FromContext[adapter.HTTPClientManager](ctx).ResolveTransport(ctx, logger, common.PtrValueOrDefault(options.Realm.HTTPClient))
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "create realm http client")
|
||||||
|
}
|
||||||
|
dnsRouter := service.FromContext[adapter.DNSRouter](ctx)
|
||||||
|
realmOptions = &realm.Options{
|
||||||
|
ServerURL: options.Realm.ServerURL,
|
||||||
|
Token: options.Realm.Token,
|
||||||
|
RealmID: options.Realm.RealmID,
|
||||||
|
STUNServers: options.Realm.STUNServers,
|
||||||
|
HTTPClient: &http.Client{Transport: httpClientTransport},
|
||||||
|
Resolver: func(ctx context.Context, host string, ipv4, ipv6 bool) ([]netip.Addr, error) {
|
||||||
|
dnsOptions := queryOptions
|
||||||
|
switch {
|
||||||
|
case ipv4 && !ipv6:
|
||||||
|
dnsOptions.Strategy = C.DomainStrategyIPv4Only
|
||||||
|
case !ipv4 && ipv6:
|
||||||
|
dnsOptions.Strategy = C.DomainStrategyIPv6Only
|
||||||
|
}
|
||||||
|
return dnsRouter.Lookup(ctx, host, dnsOptions)
|
||||||
|
},
|
||||||
|
Logger: logger,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
networkList := options.Network.Build()
|
networkList := options.Network.Build()
|
||||||
client, err := hysteria2.NewClient(hysteria2.ClientOptions{
|
client, err := hysteria2.NewClient(hysteria2.ClientOptions{
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,11 @@ import (
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing-quic/hysteria2/realm"
|
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
aTLS "github.com/sagernet/sing/common/tls"
|
aTLS "github.com/sagernet/sing/common/tls"
|
||||||
sHTTP "github.com/sagernet/sing/protocol/http"
|
sHTTP "github.com/sagernet/sing/protocol/http"
|
||||||
"github.com/sagernet/sing/service"
|
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/go-chi/chi/v5/middleware"
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
|
|
@ -31,24 +29,6 @@ func RegisterRealmService(registry *boxService.Registry) {
|
||||||
boxService.Register[option.HysteriaRealmServiceOptions](registry, C.TypeHysteriaRealm, NewRealmService)
|
boxService.Register[option.HysteriaRealmServiceOptions](registry, C.TypeHysteriaRealm, NewRealmService)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildRealmOptions(ctx context.Context, logger log.ContextLogger, options *option.Hysteria2Realm) (*realm.Options, error) {
|
|
||||||
if options == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
transport, err := service.FromContext[adapter.HTTPClientManager](ctx).ResolveTransport(ctx, logger, common.PtrValueOrDefault(options.HTTPClient))
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "create realm http client")
|
|
||||||
}
|
|
||||||
return &realm.Options{
|
|
||||||
ServerURL: options.ServerURL,
|
|
||||||
Token: options.Token,
|
|
||||||
RealmID: options.RealmID,
|
|
||||||
STUNServers: options.STUNServers,
|
|
||||||
HTTPClient: &http.Client{Transport: transport},
|
|
||||||
Logger: logger,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type RealmService struct {
|
type RealmService struct {
|
||||||
boxService.Adapter
|
boxService.Adapter
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue