v0.26.0 proxy headers for acl
This commit is contained in:
1 parent
4af072f4e5
commit
ee01cb00fe
21 files changed
+1115
-298
No files matched your search
+382
-51
@@ -1,43 +1,87 @@
|
||||
// Package netacl is the address seam of the network listeners, beside tlsx
|
||||
// and authz: a Policy refuses a peer by its address as the socket is
|
||||
// accepted, before TLS or a byte of the protocol is read. New returns nil
|
||||
// when no rule is set, and every method tolerates a nil receiver.
|
||||
// and authz: a Policy reads the PROXY header of the proxies it trusts, then
|
||||
// refuses a peer by its client's address, before TLS or a byte of the
|
||||
// protocol is read. New returns nil when nothing is set, and every method
|
||||
// tolerates a nil receiver.
|
||||
package netacl
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/lixenwraith/logwisp/internal/config"
|
||||
"github.com/lixenwraith/logwisp/internal/core"
|
||||
"github.com/lixenwraith/logwisp/internal/tlsx"
|
||||
|
||||
"github.com/lixenwraith/log"
|
||||
)
|
||||
|
||||
// warnEvery spaces the WARNs reporting refused peers; acl_denied counts all
|
||||
const warnEvery = time.Minute
|
||||
const (
|
||||
// warnEvery spaces the WARNs reporting refused peers; acl_denied counts all
|
||||
warnEvery = time.Minute
|
||||
v1Max = 107 // the longest PROXY v1 line, CRLF included
|
||||
v2Max = 2048 // the longest PROXY v2 address block, TLVs included
|
||||
)
|
||||
|
||||
var (
|
||||
sigV1 = []byte("PROXY ")
|
||||
sigV2 = []byte("\r\n\r\n\x00\r\nQUIT\n")
|
||||
// local is where a proxy_from entry lies without a startup warning
|
||||
local = []netip.Prefix{
|
||||
netip.MustParsePrefix("10.0.0.0/8"), netip.MustParsePrefix("172.16.0.0/12"),
|
||||
netip.MustParsePrefix("192.168.0.0/16"), netip.MustParsePrefix("127.0.0.0/8"),
|
||||
netip.MustParsePrefix("169.254.0.0/16"), netip.MustParsePrefix("fc00::/7"),
|
||||
netip.MustParsePrefix("fe80::/10"), netip.MustParsePrefix("::1/128"),
|
||||
}
|
||||
errScreened = errors.New("PROXY header from a peer outside proxy_from")
|
||||
// v2Families holds the family bytes v2 defines, each with the address
|
||||
// bytes taken under PROXY: TCP over IPv4 and IPv6 name the client, the
|
||||
// rest keep the socket address
|
||||
v2Families = map[byte]int{0x00: 0, 0x11: 12, 0x12: 0, 0x21: 36, 0x22: 0, 0x31: 0, 0x32: 0}
|
||||
)
|
||||
|
||||
// Policy is the compiled form of config.ACLOptions for one listener
|
||||
type Policy struct {
|
||||
allow, deny []netip.Prefix
|
||||
logger *log.Logger
|
||||
component, id string
|
||||
allow, deny, proxyFrom []netip.Prefix
|
||||
mode string // proxy_protocol, when proxy_from is set
|
||||
headerWait time.Duration // for a proxy's header: tlsx.HandshakeTimeout
|
||||
logger *log.Logger
|
||||
component, id string
|
||||
|
||||
denied atomic.Uint64
|
||||
epoch time.Time // a monotonic base: a stepped clock neither mutes nor floods the WARN
|
||||
nextWarn atomic.Int64 // nanoseconds since epoch
|
||||
denied, headers atomic.Uint64
|
||||
epoch time.Time // a monotonic base: a stepped clock neither mutes nor floods the WARN
|
||||
nextWarn atomic.Int64 // nanoseconds since epoch
|
||||
}
|
||||
|
||||
// New compiles the rules of a listener on host and warns of what they leave
|
||||
// open. Each entry must be of the family core.Network gives host: one the
|
||||
// listener's sockets can never carry is a mistake, not a rule.
|
||||
// open. Entries are of the family core.Network gives host, as one the sockets
|
||||
// never carry is a mistake; behind proxy_from, allow and deny take both, as
|
||||
// the proxy names clients of either.
|
||||
func New(o *config.ACLOptions, host string, l *log.Logger, component, id string) (*Policy, error) {
|
||||
if o == nil || len(o.Allow)+len(o.Deny) == 0 {
|
||||
if o == nil {
|
||||
return nil, nil
|
||||
}
|
||||
proxied := o.ProxyProtocol == "optional" || o.ProxyProtocol == "required"
|
||||
switch {
|
||||
case !proxied && o.ProxyProtocol != "" && o.ProxyProtocol != "off":
|
||||
return nil, fmt.Errorf("acl: proxy_protocol %q is none of off, optional and required", o.ProxyProtocol)
|
||||
case proxied && len(o.ProxyFrom) == 0:
|
||||
return nil, fmt.Errorf("acl: proxy_protocol %s needs proxy_from, the proxies that may send the header", o.ProxyProtocol)
|
||||
case !proxied && len(o.ProxyFrom) > 0:
|
||||
return nil, errors.New("acl: proxy_from needs proxy_protocol optional or required")
|
||||
case len(o.Allow)+len(o.Deny)+len(o.ProxyFrom) == 0:
|
||||
return nil, nil
|
||||
}
|
||||
network, err := core.Network(host)
|
||||
@@ -45,7 +89,7 @@ func New(o *config.ACLOptions, host string, l *log.Logger, component, id string)
|
||||
return nil, err
|
||||
}
|
||||
var widened []string // entry, network pairs
|
||||
compile := func(key string, entries []string) ([]netip.Prefix, error) {
|
||||
compile := func(key, network string, entries []string) ([]netip.Prefix, error) {
|
||||
var rules []netip.Prefix
|
||||
for _, e := range entries {
|
||||
prefix, err := parse(e, network)
|
||||
@@ -59,11 +103,17 @@ func New(o *config.ACLOptions, host string, l *log.Logger, component, id string)
|
||||
}
|
||||
return rules, nil
|
||||
}
|
||||
p := &Policy{logger: l, component: component, id: id, epoch: time.Now()}
|
||||
if p.allow, err = compile("allow", o.Allow); err != nil {
|
||||
p := &Policy{mode: o.ProxyProtocol, headerWait: tlsx.HandshakeTimeout, logger: l, component: component, id: id, epoch: time.Now()}
|
||||
if p.proxyFrom, err = compile("proxy_from", network, o.ProxyFrom); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if p.deny, err = compile("deny", o.Deny); err != nil {
|
||||
if proxied {
|
||||
network = "tcp"
|
||||
}
|
||||
if p.allow, err = compile("allow", network, o.Allow); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if p.deny, err = compile("deny", network, o.Deny); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.logStartup(host, widened)
|
||||
@@ -106,7 +156,22 @@ func (p *Policy) logStartup(host string, widened []string) {
|
||||
"hint", "list only the networks that may connect")
|
||||
}
|
||||
}
|
||||
if addr, err := netip.ParseAddr(host); len(p.allow) == 0 && (host == "" || err == nil && addr.IsUnspecified()) {
|
||||
if p.mode == "optional" {
|
||||
p.logger.Warn("msg", "acl.proxy_protocol optional: a client a proxy_from peer forwards without a header passes as the proxy",
|
||||
"component", p.component,
|
||||
"instance_id", p.id,
|
||||
"hint", "set required once every route from proxy_from sends the header")
|
||||
}
|
||||
for _, q := range p.proxyFrom {
|
||||
if !slices.ContainsFunc(local, func(l netip.Prefix) bool { return l.Bits() <= q.Bits() && l.Contains(q.Addr()) }) {
|
||||
p.logger.Warn("msg", "acl.proxy_from reaches public addresses, any of which may name any client",
|
||||
"component", p.component,
|
||||
"instance_id", p.id,
|
||||
"entry", q.String(),
|
||||
"hint", "list only the proxies' own addresses")
|
||||
}
|
||||
}
|
||||
if addr, err := netip.ParseAddr(host); len(p.allow) == 0 && len(p.deny) > 0 && (host == "" || err == nil && addr.IsUnspecified()) {
|
||||
p.logger.Warn("msg", "Listener on every address admits all that acl.deny does not list",
|
||||
"component", p.component,
|
||||
"instance_id", p.id,
|
||||
@@ -114,61 +179,323 @@ func (p *Policy) logStartup(host string, widened []string) {
|
||||
}
|
||||
}
|
||||
|
||||
// in reports whether addr lies in a rule, matched unmapped and without its
|
||||
// zone: a zoned address is in no prefix
|
||||
func in(rules []netip.Prefix, addr netip.Addr) bool {
|
||||
addr = addr.Unmap().WithZone("")
|
||||
return slices.ContainsFunc(rules, func(q netip.Prefix) bool { return q.Contains(addr) })
|
||||
}
|
||||
|
||||
// admits applies the rules: deny wins, then a set allow list admits only its
|
||||
// entries. A peer whose address is unknown is refused.
|
||||
func (p *Policy) admits(addr netip.Addr) bool {
|
||||
addr = addr.Unmap().WithZone("") // a zoned address is in no prefix
|
||||
in := func(q netip.Prefix) bool { return q.Contains(addr) }
|
||||
return addr.IsValid() && !slices.ContainsFunc(p.deny, in) &&
|
||||
(len(p.allow) == 0 || slices.ContainsFunc(p.allow, in))
|
||||
return addr.IsValid() && !in(p.deny, addr) && (len(p.allow) == 0 || in(p.allow, addr))
|
||||
}
|
||||
|
||||
// Listener returns ln refusing the peers the rules exclude as it accepts them,
|
||||
// so the plugin never sees one. Close stays ln's: a sink's shutdown flush
|
||||
// reaches it unchanged. A nil Policy returns ln.
|
||||
// proxies reports a peer in proxy_from. A zoned one never is: a link-local
|
||||
// address is any link's to claim.
|
||||
func (p *Policy) proxies(addr netip.Addr) bool {
|
||||
addr = addr.Unmap()
|
||||
return slices.ContainsFunc(p.proxyFrom, func(q netip.Prefix) bool { return q.Contains(addr) })
|
||||
}
|
||||
|
||||
func addrOf(c net.Conn) netip.Addr {
|
||||
if t, ok := c.RemoteAddr().(*net.TCPAddr); ok {
|
||||
return t.AddrPort().Addr()
|
||||
}
|
||||
return netip.Addr{}
|
||||
}
|
||||
|
||||
// Listener returns ln deciding each peer before Accept returns it. Headers are
|
||||
// read off the accept loop, so a slow proxy holds only its own connection; the
|
||||
// loop starts at once, so call this in Start. A nil Policy returns ln.
|
||||
func (p *Policy) Listener(ln net.Listener) net.Listener {
|
||||
if p == nil {
|
||||
return ln
|
||||
}
|
||||
return &listener{Listener: ln, p: p}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
l := &listener{Listener: ln, p: p, accepted: make(chan accepted), ctx: ctx, cancel: cancel}
|
||||
go l.run()
|
||||
return l
|
||||
}
|
||||
|
||||
type listener struct {
|
||||
net.Listener
|
||||
p *Policy
|
||||
p *Policy
|
||||
accepted chan accepted
|
||||
ctx context.Context // done at Close
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
type accepted struct {
|
||||
conn net.Conn
|
||||
err error
|
||||
}
|
||||
|
||||
func (l *listener) Accept() (net.Conn, error) {
|
||||
for {
|
||||
conn, err := l.Listener.Accept()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var addr netip.Addr
|
||||
remote := conn.RemoteAddr()
|
||||
if t, ok := remote.(*net.TCPAddr); ok {
|
||||
addr = t.AddrPort().Addr()
|
||||
}
|
||||
if l.p.admits(addr) {
|
||||
return conn, nil
|
||||
}
|
||||
conn.Close()
|
||||
l.p.refused(fmt.Sprint(remote))
|
||||
select {
|
||||
case a := <-l.accepted:
|
||||
return a.conn, a.err
|
||||
case <-l.ctx.Done():
|
||||
return nil, net.ErrClosed
|
||||
}
|
||||
}
|
||||
|
||||
// refused counts a refusal and WARNs at most once per warnEvery
|
||||
func (p *Policy) refused(remote string) {
|
||||
n := p.denied.Add(1)
|
||||
// Close also closes the connections still sending a header
|
||||
func (l *listener) Close() error {
|
||||
l.cancel()
|
||||
return l.Listener.Close()
|
||||
}
|
||||
|
||||
func (l *listener) run() {
|
||||
for {
|
||||
c, err := l.Listener.Accept()
|
||||
if err != nil {
|
||||
if !l.deliver(nil, err) || errors.Is(err, net.ErrClosed) {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case l.p.proxies(addrOf(c)):
|
||||
go l.proxied(c)
|
||||
case len(l.p.proxyFrom) > 0:
|
||||
l.admit(&conn{Conn: c, r: bufio.NewReaderSize(c, v1Max), screen: l.p})
|
||||
default:
|
||||
l.admit(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// proxied reads the header of a peer in proxy_from, then admits the
|
||||
// connection by the client it names
|
||||
func (l *listener) proxied(c net.Conn) {
|
||||
stop := context.AfterFunc(l.ctx, func() { c.Close() })
|
||||
c.SetReadDeadline(time.Now().Add(l.p.headerWait))
|
||||
br := bufio.NewReaderSize(c, v1Max)
|
||||
client, found, err := readHeader(br)
|
||||
c.SetReadDeadline(time.Time{})
|
||||
if !stop() {
|
||||
return // closed with the listener
|
||||
}
|
||||
pc := &conn{Conn: c, r: br}
|
||||
switch {
|
||||
case err != nil:
|
||||
l.p.refuse(c, "PROXY header: "+err.Error())
|
||||
return
|
||||
case !found && l.p.mode == "required":
|
||||
l.p.refuse(c, "no PROXY header")
|
||||
return
|
||||
case found:
|
||||
l.p.headers.Add(1)
|
||||
if client.IsValid() {
|
||||
pc.client = net.TCPAddrFromAddrPort(client)
|
||||
}
|
||||
}
|
||||
l.admit(pc)
|
||||
}
|
||||
|
||||
// admit hands c to Accept if the rules admit the address it ends with
|
||||
func (l *listener) admit(c net.Conn) {
|
||||
if !l.p.admits(addrOf(c)) {
|
||||
l.p.refuse(c, "address rules")
|
||||
return
|
||||
}
|
||||
l.deliver(c, nil)
|
||||
}
|
||||
|
||||
func (l *listener) deliver(c net.Conn, err error) bool {
|
||||
select {
|
||||
case l.accepted <- accepted{c, err}:
|
||||
return true
|
||||
case <-l.ctx.Done():
|
||||
if c != nil {
|
||||
c.Close()
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// refuse closes c, counts it and WARNs at most once per warnEvery
|
||||
func (p *Policy) refuse(c net.Conn, reason string) {
|
||||
n := p.denied.Add(1) // first: a peer that sees the close sees the count
|
||||
c.Close()
|
||||
now, next := int64(time.Since(p.epoch)), p.nextWarn.Load()
|
||||
if now < next || !p.nextWarn.CompareAndSwap(next, now+int64(warnEvery)) {
|
||||
return
|
||||
}
|
||||
p.logger.Warn("msg", "Connection refused by acl; more within a minute are only counted",
|
||||
fields := []any{"msg", "Connection refused by acl; more within a minute are only counted",
|
||||
"component", p.component,
|
||||
"instance_id", p.id,
|
||||
"remote_addr", remote,
|
||||
"acl_denied", n)
|
||||
"remote_addr", fmt.Sprint(c.RemoteAddr()),
|
||||
"reason", reason,
|
||||
"acl_denied", n}
|
||||
if peer := PeerAddr(c); peer != "" {
|
||||
fields = append(fields, "peer_addr", peer)
|
||||
}
|
||||
p.logger.Warn(fields...)
|
||||
}
|
||||
|
||||
// conn is read through the buffer its first bytes went to: past a PROXY
|
||||
// header, whose client it reports, or under a screen refusing one from a peer
|
||||
// outside proxy_from at the first Read
|
||||
type conn struct {
|
||||
net.Conn
|
||||
r *bufio.Reader
|
||||
client net.Addr // nil keeps the socket's
|
||||
screen *Policy
|
||||
once sync.Once
|
||||
err error
|
||||
}
|
||||
|
||||
func (c *conn) Read(b []byte) (int, error) {
|
||||
c.once.Do(func() {
|
||||
if c.screen == nil {
|
||||
return
|
||||
}
|
||||
if v, _ := signature(c.r); v != 0 {
|
||||
c.err = errScreened
|
||||
c.screen.refuse(c, errScreened.Error())
|
||||
}
|
||||
})
|
||||
if c.err != nil {
|
||||
return 0, c.err
|
||||
}
|
||||
return c.r.Read(b)
|
||||
}
|
||||
|
||||
func (c *conn) RemoteAddr() net.Addr {
|
||||
if c.client != nil {
|
||||
return c.client
|
||||
}
|
||||
return c.Conn.RemoteAddr()
|
||||
}
|
||||
|
||||
// PeerAddr is the proxy whose PROXY header named c's client, "" when c came
|
||||
// direct; c may be the *tls.Conn over the listener's connection
|
||||
func PeerAddr(c net.Conn) string {
|
||||
if t, ok := c.(interface{ NetConn() net.Conn }); ok {
|
||||
c = t.NetConn()
|
||||
}
|
||||
if pc, ok := c.(*conn); ok && pc.client != nil {
|
||||
return pc.Conn.RemoteAddr().String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type peerKey struct{}
|
||||
|
||||
// ConnContext, as an http.Server's, carries PeerAddr to the requests of a
|
||||
// connection for ContextPeerAddr
|
||||
func ConnContext(ctx context.Context, c net.Conn) context.Context {
|
||||
if peer := PeerAddr(c); peer != "" {
|
||||
return context.WithValue(ctx, peerKey{}, peer)
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
func ContextPeerAddr(ctx context.Context) string {
|
||||
peer, _ := ctx.Value(peerKey{}).(string)
|
||||
return peer
|
||||
}
|
||||
|
||||
// signature reads as far as tells whether a PROXY header starts the stream:
|
||||
// its version, or 0 when other bytes or none come first
|
||||
func signature(br *bufio.Reader) (int, error) {
|
||||
for n := 1; ; n++ {
|
||||
b, err := br.Peek(n)
|
||||
switch {
|
||||
case len(b) == 0 || !bytes.HasPrefix(sigV1, b) && !bytes.HasPrefix(sigV2, b):
|
||||
return 0, nil
|
||||
case bytes.Equal(b, sigV1):
|
||||
return 1, nil
|
||||
case bytes.Equal(b, sigV2):
|
||||
return 2, nil
|
||||
case err != nil:
|
||||
return 0, fmt.Errorf("signature cut short: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// readHeader reads the PROXY header starting br, if one does: found is false,
|
||||
// with nothing consumed, when none does. The client is invalid for LOCAL,
|
||||
// UNKNOWN and the families v2 leaves to the socket address.
|
||||
func readHeader(br *bufio.Reader) (client netip.AddrPort, found bool, err error) {
|
||||
v, err := signature(br)
|
||||
switch v {
|
||||
case 1:
|
||||
client, err = readV1(br)
|
||||
case 2:
|
||||
client, err = readV2(br)
|
||||
}
|
||||
return client, v != 0 && err == nil, err
|
||||
}
|
||||
|
||||
// readV1 reads "PROXY TCP4|TCP6 src dst sport dport\r\n" or "PROXY UNKNOWN...\r\n"
|
||||
func readV1(br *bufio.Reader) (netip.AddrPort, error) {
|
||||
line, err := br.ReadSlice('\n')
|
||||
if errors.Is(err, bufio.ErrBufferFull) {
|
||||
return netip.AddrPort{}, fmt.Errorf("v1 line over %d bytes", v1Max)
|
||||
} else if err != nil {
|
||||
return netip.AddrPort{}, fmt.Errorf("v1 line cut short: %w", err)
|
||||
}
|
||||
s, crlf := strings.CutSuffix(string(line), "\r\n")
|
||||
f := strings.Split(s, " ")
|
||||
switch {
|
||||
case !crlf:
|
||||
return netip.AddrPort{}, errors.New("v1 line not ended by CRLF")
|
||||
case f[1] == "UNKNOWN":
|
||||
return netip.AddrPort{}, nil
|
||||
case len(f) != 6 || f[1] != "TCP4" && f[1] != "TCP6":
|
||||
return netip.AddrPort{}, fmt.Errorf("malformed v1 line %q", s)
|
||||
}
|
||||
src, err1 := netip.ParseAddr(f[2])
|
||||
dst, err2 := netip.ParseAddr(f[3])
|
||||
port, err3 := strconv.ParseUint(f[4], 10, 16)
|
||||
_, err4 := strconv.ParseUint(f[5], 10, 16)
|
||||
four := f[1] == "TCP4"
|
||||
if errors.Join(err1, err2, err3, err4) != nil || src.Zone()+dst.Zone() != "" || src.Is4() != four || dst.Is4() != four {
|
||||
return netip.AddrPort{}, fmt.Errorf("malformed v1 line %q", s)
|
||||
}
|
||||
return netip.AddrPortFrom(src.Unmap(), uint16(port)), nil
|
||||
}
|
||||
|
||||
// readV2 reads the 16-byte prefix and the block after it, taking the address
|
||||
// of TCP over IPv4 or IPv6 and skipping the rest, TLVs included
|
||||
func readV2(br *bufio.Reader) (netip.AddrPort, error) {
|
||||
h, err := br.Peek(16)
|
||||
if err != nil {
|
||||
return netip.AddrPort{}, fmt.Errorf("v2 prefix cut short: %w", err)
|
||||
}
|
||||
cmd, fam, n := h[12], h[13], int(binary.BigEndian.Uint16(h[14:]))
|
||||
need, known := v2Families[fam]
|
||||
if cmd&0xf != 1 {
|
||||
need, known = 0, true // LOCAL ignores the block
|
||||
}
|
||||
switch {
|
||||
case cmd>>4 != 2 || cmd&0xf > 1:
|
||||
return netip.AddrPort{}, fmt.Errorf("v2 version and command %#x", cmd)
|
||||
case !known:
|
||||
return netip.AddrPort{}, fmt.Errorf("v2 family %#x", fam)
|
||||
case n > v2Max:
|
||||
return netip.AddrPort{}, fmt.Errorf("v2 block of %d bytes over %d", n, v2Max)
|
||||
case n < need:
|
||||
return netip.AddrPort{}, fmt.Errorf("v2 block of %d bytes short for family %#x", n, fam)
|
||||
}
|
||||
br.Discard(16)
|
||||
var client netip.AddrPort
|
||||
if need > 0 {
|
||||
b, err := br.Peek(need)
|
||||
if err != nil {
|
||||
return netip.AddrPort{}, fmt.Errorf("v2 block cut short: %w", err)
|
||||
}
|
||||
addr, _ := netip.AddrFromSlice(b[:(need-4)/2]) // source, destination, two ports
|
||||
client = netip.AddrPortFrom(addr.Unmap(), binary.BigEndian.Uint16(b[need-4:]))
|
||||
}
|
||||
if _, err := br.Discard(n); err != nil {
|
||||
return netip.AddrPort{}, fmt.Errorf("v2 block cut short: %w", err)
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// Describe renders the policy for log lines, stats and the status endpoint
|
||||
@@ -180,7 +507,11 @@ func (p *Policy) Describe() string {
|
||||
if len(p.allow) > 0 {
|
||||
allow = fmt.Sprint(len(p.allow))
|
||||
}
|
||||
return fmt.Sprintf("allow=%s deny=%d", allow, len(p.deny))
|
||||
s := fmt.Sprintf("allow=%s deny=%d", allow, len(p.deny))
|
||||
if len(p.proxyFrom) > 0 {
|
||||
s += fmt.Sprintf(" proxy_protocol=%s proxy_from=%d", p.mode, len(p.proxyFrom))
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (p *Policy) Denied() uint64 {
|
||||
@@ -196,5 +527,5 @@ func (p *Policy) Stats() map[string]any {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
return map[string]any{"acl": p.Describe(), "acl_denied": p.denied.Load()}
|
||||
return map[string]any{"acl": p.Describe(), "acl_denied": p.denied.Load(), "acl_proxy_headers": p.headers.Load()}
|
||||
}
|
||||
+330
-45
@@ -1,10 +1,16 @@
|
||||
package netacl
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -13,9 +19,9 @@ import (
|
||||
"github.com/lixenwraith/log"
|
||||
)
|
||||
|
||||
func mustNew(t *testing.T, host string, allow, deny []string) *Policy {
|
||||
func mustNew(t *testing.T, host string, o config.ACLOptions) *Policy {
|
||||
t.Helper()
|
||||
p, err := New(&config.ACLOptions{Allow: allow, Deny: deny}, host, log.NewLogger(), "test", "acl")
|
||||
p, err := New(&o, host, log.NewLogger(), "test", "acl")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -26,8 +32,8 @@ func mustNew(t *testing.T, host string, allow, deny []string) *Policy {
|
||||
// one admits all not denied, an IPv4-mapped peer matches as IPv4, and a peer
|
||||
// of unknown address is refused.
|
||||
func TestDenyWinsThenAllowListAdmitsOnlyItsEntries(t *testing.T) {
|
||||
listed := mustNew(t, "0.0.0.0", []string{"127.0.0.0/8"}, []string{"127.0.0.2"})
|
||||
denyOnly := mustNew(t, "0.0.0.0", nil, []string{"10.0.0.0/8"})
|
||||
listed := mustNew(t, "0.0.0.0", config.ACLOptions{Allow: []string{"127.0.0.0/8"}, Deny: []string{"127.0.0.2"}})
|
||||
denyOnly := mustNew(t, "0.0.0.0", config.ACLOptions{Deny: []string{"10.0.0.0/8"}})
|
||||
for _, c := range []struct {
|
||||
p *Policy
|
||||
addr string
|
||||
@@ -50,68 +56,351 @@ func TestDenyWinsThenAllowListAdmitsOnlyItsEntries(t *testing.T) {
|
||||
|
||||
// Entries are of the family core.Network gives the listener's host, as its
|
||||
// sockets carry no other: IPv4 on an IPv4 listener, IPv6 on an IPv6 one,
|
||||
// either behind a hostname; never IPv4-mapped, never with a zone.
|
||||
// either behind a hostname; never IPv4-mapped, never with a zone. Behind
|
||||
// proxy_from, which keeps that family, deny takes both: the proxy names either.
|
||||
func TestEntriesFollowTheListenerFamily(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
host, entry string
|
||||
ok bool
|
||||
host, from, entry string
|
||||
ok bool
|
||||
}{
|
||||
{"0.0.0.0", "10.0.0.0/8", true},
|
||||
{"0.0.0.0", "2001:db8::/32", false},
|
||||
{"::", "2001:db8::1", true},
|
||||
{"::", "10.0.0.1", false},
|
||||
{"::", "::ffff:10.0.0.1", false},
|
||||
{"::", "fe80::1%eth0", false},
|
||||
{"logs.example", "10.0.0.0/8", true},
|
||||
{"logs.example", "2001:db8::/32", true},
|
||||
{"0.0.0.0", "", "10.0.0.0/8", true},
|
||||
{"0.0.0.0", "", "2001:db8::/32", false},
|
||||
{"::", "", "2001:db8::1", true},
|
||||
{"::", "", "10.0.0.1", false},
|
||||
{"::", "", "::ffff:10.0.0.1", false},
|
||||
{"::", "", "fe80::1%eth0", false},
|
||||
{"logs.example", "", "10.0.0.0/8", true},
|
||||
{"logs.example", "", "2001:db8::/32", true},
|
||||
{"0.0.0.0", "10.0.0.5", "2001:db8::/32", true},
|
||||
{"0.0.0.0", "2001:db8::5", "10.0.0.0/8", false},
|
||||
} {
|
||||
_, err := New(&config.ACLOptions{Deny: []string{c.entry}}, c.host, log.NewLogger(), "test", "acl")
|
||||
if (err == nil) != c.ok {
|
||||
t.Errorf("%s on %q: %v", c.entry, c.host, err)
|
||||
o := &config.ACLOptions{Deny: []string{c.entry}}
|
||||
if c.from != "" {
|
||||
o.ProxyProtocol, o.ProxyFrom = "required", []string{c.from}
|
||||
}
|
||||
if _, err := New(o, c.host, log.NewLogger(), "test", "acl"); (err == nil) != c.ok {
|
||||
t.Errorf("%s behind %q on %q: %v", c.entry, c.from, c.host, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A link-local peer carries its interface zone, which no prefix contains:
|
||||
// it is matched without it, or a deny of fe80::/10 would admit it.
|
||||
func TestZonedPeerMatchesItsPrefix(t *testing.T) {
|
||||
p := mustNew(t, "::", nil, []string{"fe80::/10"})
|
||||
if p.admits(netip.MustParseAddr("fe80::1%eth0")) {
|
||||
t.Fatal("a zoned link-local peer passed the deny of its prefix")
|
||||
// A link-local peer carries its interface zone, which no prefix contains: the
|
||||
// rules match it without, or a deny of fe80::/10 would admit it. It is never
|
||||
// a proxy, as any link can claim its address.
|
||||
func TestZonedPeerMatchesRulesButIsNoProxy(t *testing.T) {
|
||||
p := mustNew(t, "::", config.ACLOptions{Deny: []string{"fe80::/10"}, ProxyProtocol: "required", ProxyFrom: []string{"fe80::/10"}})
|
||||
if zoned := netip.MustParseAddr("fe80::1%eth0"); p.admits(zoned) || p.proxies(zoned) {
|
||||
t.Fatal("a zoned link-local peer passed the deny of its prefix, or as a proxy")
|
||||
}
|
||||
}
|
||||
|
||||
// A refused peer is closed as it is accepted and counted: the plugin's
|
||||
// Accept returns the next admitted one.
|
||||
func TestRefusedPeerNeverReachesAccept(t *testing.T) {
|
||||
inner := &fakeListener{conns: make(chan net.Conn, 2)}
|
||||
refused, refusedPeer := net.Pipe()
|
||||
admitted, _ := net.Pipe()
|
||||
inner.conns <- remoteAt(refused, "192.0.2.1")
|
||||
inner.conns <- remoteAt(admitted, "127.0.0.1")
|
||||
ln := mustNew(t, "127.0.0.1", nil, []string{"192.0.2.0/24"}).Listener(inner)
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := conn.RemoteAddr().String(); !strings.HasPrefix(got, "127.0.0.1:") {
|
||||
p := mustNew(t, "127.0.0.1", config.ACLOptions{Deny: []string{"192.0.2.0/24"}})
|
||||
ln, dial := listening(t, p)
|
||||
refused := dial("192.0.2.1")
|
||||
dial("127.0.0.1")
|
||||
if got := accept(t, ln).RemoteAddr().String(); !strings.HasPrefix(got, "127.0.0.1:") {
|
||||
t.Fatalf("Accept returned %s", got)
|
||||
}
|
||||
refusedPeer.SetReadDeadline(time.Now().Add(time.Second))
|
||||
if _, err := refusedPeer.Read(make([]byte, 1)); err != io.EOF {
|
||||
t.Fatalf("the refused peer reads %v, want EOF", err)
|
||||
}
|
||||
if n := ln.(*listener).p.Stats()["acl_denied"]; n != uint64(1) {
|
||||
closed(t, refused)
|
||||
if n := p.Stats()["acl_denied"]; n != uint64(1) {
|
||||
t.Fatalf("acl_denied = %v", n)
|
||||
}
|
||||
}
|
||||
|
||||
// proxy_protocol and proxy_from come together: either alone is a mistake, as
|
||||
// is a mode that is none of off, optional and required
|
||||
func TestProxyProtocolAndProxyFromComeTogether(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
mode string
|
||||
from []string
|
||||
ok bool
|
||||
}{
|
||||
{"required", nil, false},
|
||||
{"", []string{"127.0.0.1"}, false},
|
||||
{"off", []string{"127.0.0.1"}, false},
|
||||
{"always", nil, false},
|
||||
{"optional", []string{"127.0.0.1"}, true},
|
||||
} {
|
||||
_, err := New(&config.ACLOptions{ProxyProtocol: c.mode, ProxyFrom: c.from}, "127.0.0.1", log.NewLogger(), "test", "acl")
|
||||
if (err == nil) != c.ok {
|
||||
t.Errorf("proxy_protocol %q with proxy_from %v: %v", c.mode, c.from, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A v1 or v2 header names the client, IPv4-mapped as IPv4 (as throttling
|
||||
// keys it), and leaves the bytes after it, TLVs skipped.
|
||||
func TestHeaderNamesItsClient(t *testing.T) {
|
||||
for _, c := range []struct{ header, client string }{
|
||||
{"PROXY TCP4 198.51.100.1 192.0.2.1 40000 443\r\n", "198.51.100.1:40000"},
|
||||
{"PROXY TCP6 2001:db8::1 2001:db8::2 40000 443\r\n", "[2001:db8::1]:40000"},
|
||||
{"PROXY TCP6 ::ffff:198.51.100.1 ::ffff:192.0.2.1 40000 443\r\n", "198.51.100.1:40000"},
|
||||
{v2(0x21, 0x11, block("198.51.100.1", "192.0.2.1")), "198.51.100.1:40000"},
|
||||
{v2(0x21, 0x21, block("2001:db8::1", "2001:db8::2", 0x04, 0x00, 0x01, 0xff)), "[2001:db8::1]:40000"},
|
||||
{v2(0x21, 0x21, block("::ffff:198.51.100.1", "::ffff:192.0.2.1")), "198.51.100.1:40000"},
|
||||
} {
|
||||
client, found, rest, err := parseHeader(c.header + "rest")
|
||||
if err != nil || !found || client.String() != c.client || rest != "rest" {
|
||||
t.Errorf("%q: client %v found %v rest %q: %v", c.header, client, found, rest, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LOCAL, UNKNOWN and the families v2 leaves unspecified keep the socket
|
||||
// address, whatever address block they carry
|
||||
func TestLocalAndUnknownKeepTheSocketAddress(t *testing.T) {
|
||||
for _, header := range []string{
|
||||
"PROXY UNKNOWN\r\n",
|
||||
"PROXY UNKNOWN " + strings.Repeat("x", 91) + "\r\n", // 107 bytes
|
||||
v2(0x20, 0x11, block("198.51.100.1", "192.0.2.1")),
|
||||
v2(0x20, 0x00, nil),
|
||||
v2(0x21, 0x00, nil),
|
||||
v2(0x21, 0x12, block("198.51.100.1", "192.0.2.1")),
|
||||
v2(0x21, 0x31, make([]byte, 216)),
|
||||
} {
|
||||
client, found, rest, err := parseHeader(header + "rest")
|
||||
if err != nil || !found || client.IsValid() || rest != "rest" {
|
||||
t.Errorf("%q: client %v found %v rest %q: %v", header, client, found, rest, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A malformed, oversized or truncated header is an error, never a client
|
||||
func TestMalformedHeaderIsRefused(t *testing.T) {
|
||||
inet := v2(0x21, 0x11, block("198.51.100.1", "192.0.2.1"))
|
||||
for _, header := range []string{
|
||||
"PROXY UNKNOWN 198.51.100.1 192.0.2.1 40000 443\n",
|
||||
"PROXY UNKNOWN " + strings.Repeat("x", 92) + "\r\n",
|
||||
"PROXY TCP4 2001:db8::1 192.0.2.1 40000 443\r\n",
|
||||
"PROXY TCP4 198.51.100.1 2001:db8::2 40000 443\r\n",
|
||||
"PROXY TCP6 198.51.100.1 192.0.2.1 40000 443\r\n",
|
||||
"PROXY TCP4 198.51.100.1 192.0.2.1 70000 443\r\n",
|
||||
"PROXY TCP4 198.51.100.1 192.0.2.1 40000\r\n",
|
||||
"PROXY TCP6 fe80::1%eth0 fe80::2 40000 443\r\n",
|
||||
"PROXY UDP4 198.51.100.1 192.0.2.1 40000 443\r\n",
|
||||
"PROXY TCP4 198.51.100.1 192.0.2.1 40000 443",
|
||||
"PROX",
|
||||
v2(0x11, 0x11, block("198.51.100.1", "192.0.2.1")),
|
||||
v2(0x22, 0x11, block("198.51.100.1", "192.0.2.1")),
|
||||
v2(0x21, 0x41, block("198.51.100.1", "192.0.2.1")),
|
||||
v2(0x21, 0x11, make([]byte, v2Max+1)),
|
||||
v2(0x21, 0x11, block("198.51.100.1", "192.0.2.1")[:8]) + "rest",
|
||||
inet[:14],
|
||||
inet[:20],
|
||||
} {
|
||||
if client, found, _, err := parseHeader(header); err == nil || found {
|
||||
t.Errorf("%q: client %v found %v, no error", header, client, found)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A peer outside proxy_from that sends a header is refused at its first
|
||||
// read, as the header is spoofable; its other bytes pass untouched
|
||||
func TestHeaderFromUnlistedPeerIsRefused(t *testing.T) {
|
||||
p := mustNew(t, "127.0.0.1", config.ACLOptions{ProxyProtocol: "optional", ProxyFrom: []string{"10.0.0.1"}})
|
||||
ln, dial := listening(t, p)
|
||||
for _, c := range []struct {
|
||||
send string
|
||||
refused bool
|
||||
}{
|
||||
{"PROXY TCP4 198.51.100.1 192.0.2.1 40000 443\r\n", true},
|
||||
{v2(0x21, 0x11, block("198.51.100.1", "192.0.2.1")), true},
|
||||
{"POST / HTTP/1.1\r\n", false},
|
||||
} {
|
||||
go dial("192.0.2.9").Write([]byte(c.send))
|
||||
b := make([]byte, len(c.send))
|
||||
_, err := io.ReadFull(accept(t, ln), b)
|
||||
if (err != nil) != c.refused || !c.refused && string(b) != c.send {
|
||||
t.Errorf("%q from an unlisted peer: read %q, %v", c.send, b, err)
|
||||
}
|
||||
}
|
||||
if n := p.Denied(); n != 2 {
|
||||
t.Fatalf("acl_denied = %d", n)
|
||||
}
|
||||
}
|
||||
|
||||
// Under required a peer in proxy_from must send a header; under optional
|
||||
// one that sends none keeps its socket address and its bytes
|
||||
func TestRequiredRefusesAListedPeerWithoutHeader(t *testing.T) {
|
||||
for mode, refused := range map[string]bool{"required": true, "optional": false} {
|
||||
p := mustNew(t, "127.0.0.1", config.ACLOptions{ProxyProtocol: mode, ProxyFrom: []string{"127.0.0.1"}})
|
||||
ln, dial := listening(t, p)
|
||||
peer := dial("127.0.0.1")
|
||||
peer.Write([]byte("GET /"))
|
||||
if refused {
|
||||
closed(t, peer)
|
||||
continue
|
||||
}
|
||||
conn := accept(t, ln)
|
||||
b := make([]byte, 5)
|
||||
if _, err := io.ReadFull(conn, b); err != nil || string(b) != "GET /" || conn.RemoteAddr().String() != "127.0.0.1:40000" {
|
||||
t.Errorf("optional: %s read %q: %v", conn.RemoteAddr(), b, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The client a header names is what the rules, RemoteAddr and the stats see;
|
||||
// the proxy stays as PeerAddr, under TLS too
|
||||
func TestRulesSeeTheClientAHeaderNames(t *testing.T) {
|
||||
p := mustNew(t, "127.0.0.1", config.ACLOptions{ProxyProtocol: "required", ProxyFrom: []string{"127.0.0.1"}, Deny: []string{"198.51.100.2"}})
|
||||
ln, dial := listening(t, p)
|
||||
denied := dial("127.0.0.1")
|
||||
denied.Write([]byte("PROXY TCP4 198.51.100.2 192.0.2.1 40000 443\r\n"))
|
||||
go dial("127.0.0.1").Write([]byte("PROXY TCP4 198.51.100.1 192.0.2.1 40000 443\r\nhello"))
|
||||
conn := accept(t, ln)
|
||||
b := make([]byte, 5)
|
||||
if _, err := io.ReadFull(conn, b); err != nil || string(b) != "hello" {
|
||||
t.Fatalf("read %q: %v", b, err)
|
||||
}
|
||||
if got, peer := conn.RemoteAddr().String(), PeerAddr(tls.Server(conn, nil)); got != "198.51.100.1:40000" || peer != "127.0.0.1:40000" {
|
||||
t.Fatalf("RemoteAddr %s, PeerAddr %q", got, peer)
|
||||
}
|
||||
closed(t, denied)
|
||||
if s := p.Stats(); s["acl_proxy_headers"] != uint64(2) || s["acl_denied"] != uint64(1) {
|
||||
t.Fatalf("stats %v", s)
|
||||
}
|
||||
}
|
||||
|
||||
// A header still on its way holds only its own connection, and is refused
|
||||
// at the deadline
|
||||
func TestSlowHeaderHoldsOnlyItsConnection(t *testing.T) {
|
||||
p := mustNew(t, "127.0.0.1", config.ACLOptions{ProxyProtocol: "required", ProxyFrom: []string{"127.0.0.1"}})
|
||||
p.headerWait = time.Second
|
||||
ln, dial := listening(t, p)
|
||||
slow := dial("127.0.0.1")
|
||||
slow.Write([]byte("PROXY TCP4 198"))
|
||||
dial("192.0.2.9")
|
||||
if got := accept(t, ln).RemoteAddr().String(); !strings.HasPrefix(got, "192.0.2.9:") || p.Denied() != 0 {
|
||||
t.Fatalf("Accept returned %s after %d refusals", got, p.Denied())
|
||||
}
|
||||
closed(t, slow)
|
||||
if n := p.Denied(); n != 1 {
|
||||
t.Fatalf("acl_denied = %d", n)
|
||||
}
|
||||
}
|
||||
|
||||
// Close also closes the connections whose header has not arrived
|
||||
func TestCloseReleasesHeadersInFlight(t *testing.T) {
|
||||
p := mustNew(t, "127.0.0.1", config.ACLOptions{ProxyProtocol: "required", ProxyFrom: []string{"127.0.0.1"}})
|
||||
ln, dial := listening(t, p)
|
||||
slow := dial("127.0.0.1")
|
||||
slow.Write([]byte("PROXY "))
|
||||
ln.Close()
|
||||
closed(t, slow)
|
||||
if _, err := ln.Accept(); !errors.Is(err, net.ErrClosed) {
|
||||
t.Fatalf("Accept after Close: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Whatever the bytes, the parser neither panics nor reads past a header's
|
||||
// bound, consumes nothing when no header starts the stream, and names a
|
||||
// client only unmapped and without a zone.
|
||||
func FuzzReadHeader(f *testing.F) {
|
||||
for _, seed := range []string{
|
||||
"PROXY TCP4 198.51.100.1 192.0.2.1 40000 443\r\nrest",
|
||||
"PROXY TCP6 2001:db8::1 2001:db8::2 40000 443\r\n",
|
||||
"PROXY UNKNOWN\r\n",
|
||||
v2(0x21, 0x11, block("198.51.100.1", "192.0.2.1")),
|
||||
v2(0x21, 0x21, block("2001:db8::1", "2001:db8::2", 0x04, 0x00, 0x01, 0xff)),
|
||||
v2(0x20, 0x00, nil),
|
||||
"GET / HTTP/1.1\r\n",
|
||||
} {
|
||||
f.Add([]byte(seed))
|
||||
}
|
||||
f.Fuzz(func(t *testing.T, data []byte) {
|
||||
r := bytes.NewReader(data)
|
||||
br := bufio.NewReaderSize(r, v1Max)
|
||||
client, found, err := readHeader(br)
|
||||
consumed := len(data) - br.Buffered() - r.Len()
|
||||
a := client.Addr()
|
||||
switch {
|
||||
case consumed > 16+v2Max:
|
||||
t.Fatalf("consumed %d bytes", consumed)
|
||||
case err == nil && !found && consumed != 0:
|
||||
t.Fatalf("consumed %d bytes with no header", consumed)
|
||||
case client.IsValid() && (!found || a.Is4In6() || a.Zone() != ""):
|
||||
t.Fatalf("client %v, found %v", client, found)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func parseHeader(s string) (client netip.AddrPort, found bool, rest string, err error) {
|
||||
br := bufio.NewReaderSize(strings.NewReader(s), v1Max)
|
||||
client, found, err = readHeader(br)
|
||||
b, _ := io.ReadAll(br)
|
||||
return client, found, string(b), err
|
||||
}
|
||||
|
||||
// v2 is a PROXY v2 header: version and command, family, then the block
|
||||
func v2(cmd, fam byte, block []byte) string {
|
||||
h := append(slices.Clone(sigV2), cmd, fam, byte(len(block)>>8), byte(len(block)))
|
||||
return string(append(h, block...))
|
||||
}
|
||||
|
||||
// block is a v2 address block from src:40000 to dst:443, then tlv
|
||||
func block(src, dst string, tlv ...byte) []byte {
|
||||
b := append(netip.MustParseAddr(src).AsSlice(), netip.MustParseAddr(dst).AsSlice()...)
|
||||
return append(append(b, 0x9c, 0x40, 0x01, 0xbb), tlv...)
|
||||
}
|
||||
|
||||
// listening runs p's listener over a fake one; dial connects a peer from
|
||||
// addr:40000 and returns its end
|
||||
func listening(t *testing.T, p *Policy) (net.Listener, func(addr string) net.Conn) {
|
||||
inner := &fakeListener{conns: make(chan net.Conn, 4)}
|
||||
ln := p.Listener(inner)
|
||||
t.Cleanup(func() { ln.Close() })
|
||||
return ln, func(addr string) net.Conn {
|
||||
server, client := net.Pipe()
|
||||
t.Cleanup(func() { client.Close() })
|
||||
inner.conns <- remoteConn{server, net.TCPAddrFromAddrPort(netip.AddrPortFrom(netip.MustParseAddr(addr), 40000))}
|
||||
return client
|
||||
}
|
||||
}
|
||||
|
||||
func accept(t *testing.T, ln net.Listener) net.Conn {
|
||||
t.Helper()
|
||||
got := make(chan net.Conn, 1)
|
||||
go func() { c, _ := ln.Accept(); got <- c }()
|
||||
select {
|
||||
case c := <-got:
|
||||
if c == nil {
|
||||
t.Fatal("Accept failed")
|
||||
}
|
||||
t.Cleanup(func() { c.Close() })
|
||||
c.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
return c
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("Accept returned nothing")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// closed waits for the listener to close peer's connection
|
||||
func closed(t *testing.T, peer net.Conn) {
|
||||
t.Helper()
|
||||
peer.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
if _, err := peer.Read(make([]byte, 1)); err != io.EOF {
|
||||
t.Fatalf("the peer reads %v, want EOF", err)
|
||||
}
|
||||
}
|
||||
|
||||
type fakeListener struct {
|
||||
net.Listener
|
||||
conns chan net.Conn
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func (f *fakeListener) Accept() (net.Conn, error) { return <-f.conns, nil }
|
||||
func (f *fakeListener) Accept() (net.Conn, error) {
|
||||
if c, ok := <-f.conns; ok {
|
||||
return c, nil
|
||||
}
|
||||
return nil, net.ErrClosed
|
||||
}
|
||||
|
||||
func (f *fakeListener) Close() error { f.once.Do(func() { close(f.conns) }); return nil }
|
||||
|
||||
type remoteConn struct {
|
||||
net.Conn
|
||||
@@ -119,7 +408,3 @@ type remoteConn struct {
|
||||
}
|
||||
|
||||
func (c remoteConn) RemoteAddr() net.Addr { return c.remote }
|
||||
|
||||
func remoteAt(c net.Conn, addr string) net.Conn {
|
||||
return remoteConn{c, net.TCPAddrFromAddrPort(netip.AddrPortFrom(netip.MustParseAddr(addr), 40000))}
|
||||
}
|
||||
Reference in new issue
Block a user