mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-16 15:17:14 +00:00
fix(amneziawg): honor inbound listen when binding UDP socket (#6461)
* fix(amneziawg): honor inbound listen when binding UDP socket AmneziaWG inbounds ignored the listen field and always opened awgconn.NewDefaultBind(), so multi-IP hosts replied from the primary address and handshakes to a secondary IP never completed (#6367). Carry Inbound.Listen on amneziawg.Instance, open a Bind pinned to that address (wildcard when empty/0.0.0.0/::), and include listen in addressFingerprint so edits rebuild the Device. Fixes #6367 * fix(amneziawg): fall back to wildcard when listen is unusable Invalid or non-local listen values no longer hard-fail inbound startup; treat ::0/[::0] as wildcards and normalize listen in the bind fingerprint. * fix(amneziawg): use ListenConfig.ListenPacket for noctx --------- Co-authored-by: mrchatam <mrchatam@users.noreply.github.com> Co-authored-by: mrchatam <287639636+mrchatam@users.noreply.github.com>
This commit is contained in:
@@ -63,6 +63,7 @@ func InstanceFromInbound(ib *model.Inbound) (Instance, bool) {
|
||||
Tag: ib.Tag,
|
||||
InterfaceName: interfaceNameForID(ib.Id),
|
||||
ListenPort: ib.Port,
|
||||
Listen: ib.Listen,
|
||||
PrivateKey: server.PrivateKey,
|
||||
PublicKey: server.PublicKey,
|
||||
Address: addresses,
|
||||
|
||||
@@ -32,7 +32,7 @@ func TestInstanceFromInboundParsesEnabledPeers(t *testing.T) {
|
||||
{Email: "c@x", Enable: true, PublicKey: "", AllowedIPs: []string{"10.8.1.4/32"}}, // no key: skipped
|
||||
{Email: "d@x", Enable: true, PublicKey: "pubD", AllowedIPs: nil}, // no address: skipped
|
||||
})
|
||||
ib := &model.Inbound{Id: 7, Tag: "awg-tag", Protocol: model.AmneziaWG, Port: 51820, Settings: settings}
|
||||
ib := &model.Inbound{Id: 7, Tag: "awg-tag", Protocol: model.AmneziaWG, Port: 51820, Listen: "203.0.113.10", Settings: settings}
|
||||
|
||||
inst, ok := InstanceFromInbound(ib)
|
||||
if !ok {
|
||||
@@ -41,6 +41,9 @@ func TestInstanceFromInboundParsesEnabledPeers(t *testing.T) {
|
||||
if inst.Id != 7 || inst.Tag != "awg-tag" || inst.ListenPort != 51820 {
|
||||
t.Fatalf("instance identity not carried over: %+v", inst)
|
||||
}
|
||||
if inst.Listen != "203.0.113.10" {
|
||||
t.Fatalf("Listen = %q, want inbound listen carried through", inst.Listen)
|
||||
}
|
||||
if inst.InterfaceName != "awg7" {
|
||||
t.Fatalf("InterfaceName = %q, want awg7", inst.InterfaceName)
|
||||
}
|
||||
|
||||
@@ -75,8 +75,11 @@ type Instance struct {
|
||||
Tag string
|
||||
InterfaceName string
|
||||
ListenPort int
|
||||
PrivateKey string
|
||||
PublicKey string
|
||||
// Listen is an optional host bind address (e.g. "203.0.113.10").
|
||||
// Empty/wildcard keeps dual-stack StdNetBind; a real IP pins the UDP socket.
|
||||
Listen string
|
||||
PrivateKey string
|
||||
PublicKey string
|
||||
// Address holds the interface's own tunnel address(es), e.g. "10.8.1.1/24".
|
||||
// Carries both the IPv4 and (when enabled) IPv6 server address.
|
||||
Address []string
|
||||
|
||||
@@ -114,7 +114,8 @@ func newUnconfiguredClientDevice(inst amneziawg.OutboundInstance, opts DeviceOpt
|
||||
if logger == nil {
|
||||
logger = device.NewLogger(device.LogLevelSilent, fmt.Sprintf("(awg-out %s) ", inst.Tag))
|
||||
}
|
||||
dev := device.NewDevice(tun, newResolvingBind(), logger)
|
||||
bind := newResolvingBind("")
|
||||
dev := device.NewDevice(tun, bind, logger)
|
||||
|
||||
return &Device{Device: dev, Stack: gstack, localAddrs: addrs}, nil
|
||||
}
|
||||
|
||||
@@ -134,7 +134,8 @@ func newUnconfiguredDevice(inst amneziawg.Instance, opts DeviceOptions) (*Device
|
||||
if logger == nil {
|
||||
logger = device.NewLogger(device.LogLevelSilent, "")
|
||||
}
|
||||
dev := device.NewDevice(tun, newResolvingBind(), logger)
|
||||
bind := newResolvingBind(inst.Listen)
|
||||
dev := device.NewDevice(tun, bind, logger)
|
||||
|
||||
return &Device{Device: dev, Stack: gstack, localAddrs: addrs}, nil
|
||||
}
|
||||
|
||||
@@ -257,12 +257,12 @@ func socksRelayForInstance(inst amneziawg.Instance) SocksRelay {
|
||||
}
|
||||
}
|
||||
|
||||
// addressFingerprint captures what IpcSet can't change on a running Device,
|
||||
// fixed when the netstack is built: address, and the S4-derived effective MTU.
|
||||
// addressFingerprint captures Bind/netstack identity IpcSet cannot change.
|
||||
func addressFingerprint(inst amneziawg.Instance) string {
|
||||
return fmt.Sprintf("%d|%s",
|
||||
return fmt.Sprintf("%d|%s|%s",
|
||||
amneziawg.EffectiveMTU(inst.MTU, inst.Obfuscation.S4),
|
||||
strings.Join(inst.Address, ","))
|
||||
strings.Join(inst.Address, ","),
|
||||
normalizedListenFP(inst.Listen))
|
||||
}
|
||||
|
||||
// Reconcile brings every desired instance's embedded interface up to date
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
package amneziawgnet
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
awgconn "github.com/amnezia-vpn/amneziawg-go/v3/conn"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
||||
)
|
||||
|
||||
// pinnedBind opens its UDP socket on exactly one host address (#6367).
|
||||
// Empty/wildcard listen still uses StdNetBind via newListenBind.
|
||||
type pinnedBind struct {
|
||||
mu sync.Mutex
|
||||
addr netip.Addr
|
||||
conn *net.UDPConn
|
||||
}
|
||||
|
||||
func newPinnedBind(addr netip.Addr) *pinnedBind {
|
||||
return &pinnedBind{addr: addr.Unmap()}
|
||||
}
|
||||
|
||||
func (b *pinnedBind) Open(uport uint16) ([]awgconn.ReceiveFunc, uint16, error) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.conn != nil {
|
||||
return nil, 0, awgconn.ErrBindAlreadyOpen
|
||||
}
|
||||
|
||||
network := "udp4"
|
||||
if b.addr.Is6() {
|
||||
network = "udp6"
|
||||
}
|
||||
pc, err := (&net.ListenConfig{}).ListenPacket(context.Background(), network, net.JoinHostPort(b.addr.String(), strconv.Itoa(int(uport))))
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
uc, ok := pc.(*net.UDPConn)
|
||||
if !ok {
|
||||
pc.Close()
|
||||
return nil, 0, fmt.Errorf("amneziawgnet: listen %s returned %T, want *net.UDPConn", network, pc)
|
||||
}
|
||||
laddr, ok := uc.LocalAddr().(*net.UDPAddr)
|
||||
if !ok {
|
||||
uc.Close()
|
||||
return nil, 0, fmt.Errorf("amneziawgnet: unexpected local addr %T", uc.LocalAddr())
|
||||
}
|
||||
b.conn = uc
|
||||
return []awgconn.ReceiveFunc{b.makeReceiveFunc(uc)}, uint16(laddr.Port), nil
|
||||
}
|
||||
|
||||
func (b *pinnedBind) makeReceiveFunc(uc *net.UDPConn) awgconn.ReceiveFunc {
|
||||
return func(bufs [][]byte, sizes []int, eps []awgconn.Endpoint) (int, error) {
|
||||
n, addr, err := uc.ReadFromUDPAddrPort(bufs[0])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
sizes[0] = n
|
||||
eps[0] = &awgconn.StdNetEndpoint{AddrPort: netip.AddrPortFrom(addr.Addr().Unmap(), addr.Port())}
|
||||
return 1, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (b *pinnedBind) Close() error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.conn == nil {
|
||||
return nil
|
||||
}
|
||||
err := b.conn.Close()
|
||||
b.conn = nil
|
||||
return err
|
||||
}
|
||||
|
||||
// SetMark is a no-op: the panel never configures a WireGuard fwmark here.
|
||||
func (b *pinnedBind) SetMark(uint32) error { return nil }
|
||||
|
||||
func (b *pinnedBind) Send(bufs [][]byte, ep awgconn.Endpoint) error {
|
||||
std, ok := ep.(*awgconn.StdNetEndpoint)
|
||||
if !ok {
|
||||
return awgconn.ErrWrongEndpointType
|
||||
}
|
||||
b.mu.Lock()
|
||||
uc := b.conn
|
||||
b.mu.Unlock()
|
||||
if uc == nil {
|
||||
return net.ErrClosed
|
||||
}
|
||||
for _, buf := range bufs {
|
||||
if _, err := uc.WriteToUDPAddrPort(buf, std.AddrPort); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *pinnedBind) ParseEndpoint(s string) (awgconn.Endpoint, error) {
|
||||
ap, err := netip.ParseAddrPort(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &awgconn.StdNetEndpoint{AddrPort: netip.AddrPortFrom(ap.Addr().Unmap(), ap.Port())}, nil
|
||||
}
|
||||
|
||||
func (b *pinnedBind) BatchSize() int { return 1 }
|
||||
|
||||
// isWildcardListen reports empty / dual-stack wildcard listen values.
|
||||
// Includes ::0 (isAnyListen) and [::] so AmneziaWG keeps dual-stack StdNetBind.
|
||||
func isWildcardListen(listen string) bool {
|
||||
switch strings.TrimSpace(listen) {
|
||||
case "", "0.0.0.0", "::", "::0", "[::]", "[::0]":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// parseListenAddr returns a concrete host address to pin. ok is false for
|
||||
// wildcards and for values that are not a bare IP (previously inert for AWG).
|
||||
func parseListenAddr(listen string) (addr netip.Addr, ok bool) {
|
||||
listen = strings.TrimSpace(listen)
|
||||
if isWildcardListen(listen) {
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
// Bracketed IPv6 literal e.g. [::1] — strip for ParseAddr.
|
||||
if strings.HasPrefix(listen, "[") && strings.HasSuffix(listen, "]") {
|
||||
listen = listen[1 : len(listen)-1]
|
||||
}
|
||||
addr, err := netip.ParseAddr(listen)
|
||||
if err != nil {
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
return addr.Unmap(), true
|
||||
}
|
||||
|
||||
// listenBindable probes whether addr can be used as a UDP local address.
|
||||
func listenBindable(addr netip.Addr) bool {
|
||||
network := "udp4"
|
||||
if addr.Is6() {
|
||||
network = "udp6"
|
||||
}
|
||||
pc, err := (&net.ListenConfig{}).ListenPacket(context.Background(), network, net.JoinHostPort(addr.String(), "0"))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_ = pc.Close()
|
||||
return true
|
||||
}
|
||||
|
||||
// newListenBind returns StdNetBind for wildcards / unusable listen values, or
|
||||
// a pinnedBind for a real local address. Never fails the inbound on bad listen.
|
||||
func newListenBind(listen string) awgconn.Bind {
|
||||
raw := strings.TrimSpace(listen)
|
||||
addr, pinned := parseListenAddr(raw)
|
||||
if !pinned {
|
||||
if raw != "" && !isWildcardListen(raw) {
|
||||
logger.Warningf("amneziawgnet: listen %q is not a bindable IP; using dual-stack wildcard", raw)
|
||||
}
|
||||
return awgconn.NewDefaultBind()
|
||||
}
|
||||
if !listenBindable(addr) {
|
||||
logger.Warningf("amneziawgnet: listen %q is not usable on this host; using dual-stack wildcard", raw)
|
||||
return awgconn.NewDefaultBind()
|
||||
}
|
||||
return newPinnedBind(addr)
|
||||
}
|
||||
|
||||
// normalizedListenFP collapses wildcard spellings so fingerprint rebuilds
|
||||
// only when the effective Bind actually changes.
|
||||
func normalizedListenFP(listen string) string {
|
||||
if isWildcardListen(listen) {
|
||||
return ""
|
||||
}
|
||||
addr, ok := parseListenAddr(listen)
|
||||
if !ok {
|
||||
return "" // unusable → same Bind as wildcard fallback
|
||||
}
|
||||
if !listenBindable(addr) {
|
||||
return ""
|
||||
}
|
||||
return addr.String()
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package amneziawgnet
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
awgconn "github.com/amnezia-vpn/amneziawg-go/v3/conn"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
|
||||
)
|
||||
|
||||
func TestParseListenAddr(t *testing.T) {
|
||||
cases := []struct {
|
||||
in string
|
||||
pinned bool
|
||||
want string
|
||||
}{
|
||||
{in: "", pinned: false},
|
||||
{in: " ", pinned: false},
|
||||
{in: "0.0.0.0", pinned: false},
|
||||
{in: "::", pinned: false},
|
||||
{in: "::0", pinned: false},
|
||||
{in: "[::]", pinned: false},
|
||||
{in: "[::0]", pinned: false},
|
||||
{in: "127.0.0.1", pinned: true, want: "127.0.0.1"},
|
||||
{in: "::1", pinned: true, want: "::1"},
|
||||
{in: "[::1]", pinned: true, want: "::1"},
|
||||
{in: "not-an-ip", pinned: false},
|
||||
{in: "/var/run/awg.sock", pinned: false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
addr, ok := parseListenAddr(tc.in)
|
||||
if ok != tc.pinned {
|
||||
t.Fatalf("parseListenAddr(%q) pinned=%v, want %v", tc.in, ok, tc.pinned)
|
||||
}
|
||||
if tc.pinned && addr.String() != tc.want {
|
||||
t.Fatalf("parseListenAddr(%q) = %s, want %s", tc.in, addr, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewListenBindPinsSpecificAddress(t *testing.T) {
|
||||
bind := newListenBind("127.0.0.1")
|
||||
pb, ok := bind.(*pinnedBind)
|
||||
if !ok {
|
||||
t.Fatalf("bind type = %T, want *pinnedBind", bind)
|
||||
}
|
||||
fns, port, err := pb.Open(0)
|
||||
if err != nil {
|
||||
t.Fatalf("Open: %v", err)
|
||||
}
|
||||
defer pb.Close()
|
||||
if len(fns) != 1 {
|
||||
t.Fatalf("ReceiveFuncs = %d, want 1", len(fns))
|
||||
}
|
||||
if port == 0 {
|
||||
t.Fatal("expected a concrete ephemeral port")
|
||||
}
|
||||
|
||||
laddr := pb.conn.LocalAddr().(*net.UDPAddr)
|
||||
got := laddr.AddrPort().Addr().Unmap()
|
||||
if got.String() != "127.0.0.1" {
|
||||
t.Fatalf("LocalAddr = %v, want 127.0.0.1", got)
|
||||
}
|
||||
|
||||
clash := newListenBind("127.0.0.1")
|
||||
if _, _, err := clash.Open(port); err == nil {
|
||||
clash.Close()
|
||||
t.Fatalf("Open(%d) unexpectedly succeeded on an already-bound address", port)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewListenBindWildcardUsesDefault(t *testing.T) {
|
||||
for _, listen := range []string{"", "0.0.0.0", "::", "::0", "[::]", "hostname.example", "203.0.113.10", "not-an-ip"} {
|
||||
bind := newListenBind(listen)
|
||||
if _, ok := bind.(*pinnedBind); ok {
|
||||
t.Fatalf("newListenBind(%q) returned pinnedBind, want default StdNetBind", listen)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPinnedBindRoundTrip(t *testing.T) {
|
||||
server := newListenBind("127.0.0.1")
|
||||
recvFns, port, err := server.Open(0)
|
||||
if err != nil {
|
||||
t.Fatalf("server Open: %v", err)
|
||||
}
|
||||
defer server.Close()
|
||||
|
||||
client, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)})
|
||||
if err != nil {
|
||||
t.Fatalf("client listen: %v", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
payload := []byte("hello-awg-listen")
|
||||
dst := net.JoinHostPort("127.0.0.1", strconv.Itoa(int(port)))
|
||||
ap, err := netip.ParseAddrPort(dst)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseAddrPort: %v", err)
|
||||
}
|
||||
if _, err := client.WriteToUDPAddrPort(payload, ap); err != nil {
|
||||
t.Fatalf("client write: %v", err)
|
||||
}
|
||||
|
||||
bufs := [][]byte{make([]byte, 1500)}
|
||||
sizes := make([]int, 1)
|
||||
eps := make([]awgconn.Endpoint, 1)
|
||||
n, err := recvFns[0](bufs, sizes, eps)
|
||||
if err != nil {
|
||||
t.Fatalf("receive: %v", err)
|
||||
}
|
||||
if n != 1 || sizes[0] != len(payload) {
|
||||
t.Fatalf("receive n=%d size=%d, want 1/%d", n, sizes[0], len(payload))
|
||||
}
|
||||
if string(bufs[0][:sizes[0]]) != string(payload) {
|
||||
t.Fatalf("payload = %q, want %q", bufs[0][:sizes[0]], payload)
|
||||
}
|
||||
|
||||
reply := []byte("pong")
|
||||
if err := server.Send([][]byte{reply}, eps[0]); err != nil {
|
||||
t.Fatalf("Send: %v", err)
|
||||
}
|
||||
_ = client.SetReadDeadline(time.Now().Add(2 * time.Second))
|
||||
buf := make([]byte, 1500)
|
||||
rn, _, err := client.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
t.Fatalf("client read: %v", err)
|
||||
}
|
||||
if string(buf[:rn]) != string(reply) {
|
||||
t.Fatalf("reply = %q, want %q", buf[:rn], reply)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddressFingerprintIncludesListen(t *testing.T) {
|
||||
base := amneziawg.Instance{
|
||||
MTU: 1420,
|
||||
Address: []string{"10.8.1.1/24"},
|
||||
Obfuscation: amneziawg.Obfuscation31{},
|
||||
}
|
||||
a := addressFingerprint(base)
|
||||
base.Listen = "127.0.0.1"
|
||||
b := addressFingerprint(base)
|
||||
if a == b {
|
||||
t.Fatalf("listen edit did not change addressFingerprint: %q", a)
|
||||
}
|
||||
base.Listen = "0.0.0.0"
|
||||
if addressFingerprint(base) != a {
|
||||
t.Fatal("wildcard spellings must share the empty-listen fingerprint")
|
||||
}
|
||||
base.Listen = "hostname.example"
|
||||
if addressFingerprint(base) != a {
|
||||
t.Fatal("unusable listen must fingerprint like wildcard fallback")
|
||||
}
|
||||
}
|
||||
|
||||
var _ awgconn.Bind = (*pinnedBind)(nil)
|
||||
@@ -15,8 +15,8 @@ import (
|
||||
// endpointResolveTimeout bounds the one-time DNS lookup in ParseEndpoint.
|
||||
const endpointResolveTimeout = 5 * time.Second
|
||||
|
||||
// resolvingBind lets peer endpoints be hostnames: StdNetBind has no DNS and
|
||||
// an unresolved name kills the whole IpcSet. Resolved once at configure.
|
||||
// resolvingBind wraps a Bind so peer endpoints may be hostnames (#6367).
|
||||
// Concrete listen values use pinnedBind; wildcards keep StdNetBind.
|
||||
type resolvingBind struct {
|
||||
awgconn.Bind
|
||||
}
|
||||
@@ -35,8 +35,8 @@ func defaultLookupEndpointHost(ctx context.Context, host string) ([]netip.Addr,
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func newResolvingBind() *resolvingBind {
|
||||
return &resolvingBind{Bind: awgconn.NewDefaultBind()}
|
||||
func newResolvingBind(listen string) *resolvingBind {
|
||||
return &resolvingBind{Bind: newListenBind(listen)}
|
||||
}
|
||||
|
||||
// ParseEndpoint resolves hostnames before handing the address to amneziawg-go
|
||||
|
||||
@@ -9,6 +9,11 @@ import (
|
||||
awgconn "github.com/amnezia-vpn/amneziawg-go/v3/conn"
|
||||
)
|
||||
|
||||
func mustResolvingBind(t *testing.T) *resolvingBind {
|
||||
t.Helper()
|
||||
return newResolvingBind("")
|
||||
}
|
||||
|
||||
func endpointAddrPort(ep awgconn.Endpoint) netip.AddrPort {
|
||||
std, ok := ep.(*awgconn.StdNetEndpoint)
|
||||
if !ok {
|
||||
@@ -18,7 +23,7 @@ func endpointAddrPort(ep awgconn.Endpoint) netip.AddrPort {
|
||||
}
|
||||
|
||||
func TestResolvingBind_ParseEndpointIPLiteral(t *testing.T) {
|
||||
b := newResolvingBind()
|
||||
b := mustResolvingBind(t)
|
||||
ep, err := b.ParseEndpoint("203.0.113.7:51820")
|
||||
if err != nil {
|
||||
t.Fatalf("IP endpoint rejected: %v", err)
|
||||
@@ -39,7 +44,7 @@ func TestResolvingBind_ParseEndpointHostnameResolves(t *testing.T) {
|
||||
}
|
||||
defer func() { lookupEndpointHost = orig }()
|
||||
|
||||
b := newResolvingBind()
|
||||
b := mustResolvingBind(t)
|
||||
ep, err := b.ParseEndpoint("peer.example.test:443")
|
||||
if err != nil {
|
||||
t.Fatalf("hostname endpoint rejected: %v", err)
|
||||
@@ -56,14 +61,14 @@ func TestResolvingBind_ParseEndpointResolveFailureIsAnError(t *testing.T) {
|
||||
}
|
||||
defer func() { lookupEndpointHost = orig }()
|
||||
|
||||
b := newResolvingBind()
|
||||
b := mustResolvingBind(t)
|
||||
if _, err := b.ParseEndpoint("missing.example.test:80"); err == nil {
|
||||
t.Fatal("expected resolve failure to surface as an error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvingBind_ParseEndpointBadPortRejected(t *testing.T) {
|
||||
b := newResolvingBind()
|
||||
b := mustResolvingBind(t)
|
||||
if _, err := b.ParseEndpoint("203.0.113.7:none"); err == nil {
|
||||
t.Fatal("expected bad port to be rejected")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user