245 lines
6.7 KiB
Go
245 lines
6.7 KiB
Go
|
|
package main
|
||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"encoding/binary"
|
||
|
|
"io"
|
||
|
|
"net"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
type proxyRecord struct {
|
||
|
|
header []byte
|
||
|
|
payload []byte
|
||
|
|
peer net.IP
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMailTarget(t *testing.T) {
|
||
|
|
tunnel, err := parseCIDR("10.153.238.225/30")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
target, upstream, err := mailProxyTarget("stalwart-relay.stalwart.svc.cluster.local", tunnel)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if !target.address.Equal(tunnel.IP) || upstream != "stalwart-relay.stalwart.svc.cluster.local" {
|
||
|
|
t.Fatalf("unexpected proxy target: %#v %q", target, upstream)
|
||
|
|
}
|
||
|
|
if len(target.ports) != 3 || target.ports[0] != 25 || target.ports[1] != 465 || target.ports[2] != 993 {
|
||
|
|
t.Fatalf("unexpected proxy ports: %v", target.ports)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMailProxyTCP(t *testing.T) {
|
||
|
|
upstream := listenTCP(t)
|
||
|
|
listener := listenTCP(t)
|
||
|
|
failures := make(chan error, 1)
|
||
|
|
proxy := newMailProxy(listener, upstream.Addr().String(), net.ParseIP("127.0.0.2"), failures, make(chan struct{}, mailConnectionLimit))
|
||
|
|
go proxy.serve()
|
||
|
|
defer proxy.close()
|
||
|
|
records := make(chan proxyRecord, 1)
|
||
|
|
go func() {
|
||
|
|
connection, err := upstream.Accept()
|
||
|
|
if err != nil {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
defer connection.Close()
|
||
|
|
header := make([]byte, 28)
|
||
|
|
if _, err := io.ReadFull(connection, header); err != nil {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
payload := make([]byte, 4)
|
||
|
|
if _, err := io.ReadFull(connection, payload); err != nil {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
records <- proxyRecord{header: header, payload: payload, peer: connection.RemoteAddr().(*net.TCPAddr).IP}
|
||
|
|
_, _ = connection.Write([]byte("pong"))
|
||
|
|
}()
|
||
|
|
client, err := net.DialTCP("tcp4", nil, listener.Addr().(*net.TCPAddr))
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
defer client.Close()
|
||
|
|
if _, err := client.Write([]byte("ping")); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
reply := make([]byte, 4)
|
||
|
|
if _, err := io.ReadFull(client, reply); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if string(reply) != "pong" {
|
||
|
|
t.Fatalf("reply = %q", reply)
|
||
|
|
}
|
||
|
|
select {
|
||
|
|
case record := <-records:
|
||
|
|
assertProxyRecord(t, record, client.LocalAddr().(*net.TCPAddr), listener.Addr().(*net.TCPAddr))
|
||
|
|
case <-time.After(time.Second):
|
||
|
|
t.Fatal("upstream did not receive the proxied stream")
|
||
|
|
}
|
||
|
|
select {
|
||
|
|
case err := <-failures:
|
||
|
|
t.Fatalf("unexpected proxy failure: %v", err)
|
||
|
|
default:
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMailDialFailure(t *testing.T) {
|
||
|
|
unavailable := listenTCP(t)
|
||
|
|
upstream := unavailable.Addr().String()
|
||
|
|
_ = unavailable.Close()
|
||
|
|
listener := listenTCP(t)
|
||
|
|
failures := make(chan error, 1)
|
||
|
|
proxy := newMailProxy(listener, upstream, nil, failures, make(chan struct{}, mailConnectionLimit))
|
||
|
|
go proxy.serve()
|
||
|
|
defer proxy.close()
|
||
|
|
client, err := net.Dial("tcp4", listener.Addr().String())
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
defer client.Close()
|
||
|
|
if err := client.SetReadDeadline(time.Now().Add(time.Second)); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if _, err := client.Read(make([]byte, 1)); err == nil {
|
||
|
|
t.Fatal("client remained open after the upstream refusal")
|
||
|
|
}
|
||
|
|
select {
|
||
|
|
case err := <-failures:
|
||
|
|
t.Fatalf("connection-level dial failure was reported as relay-fatal: %v", err)
|
||
|
|
default:
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMailProxyClose(t *testing.T) {
|
||
|
|
upstream := listenTCP(t)
|
||
|
|
listener := listenTCP(t)
|
||
|
|
proxy := newMailProxy(listener, upstream.Addr().String(), nil, make(chan error, 1), make(chan struct{}, mailConnectionLimit))
|
||
|
|
go proxy.serve()
|
||
|
|
accepted := make(chan net.Conn, 1)
|
||
|
|
go func() {
|
||
|
|
connection, err := upstream.Accept()
|
||
|
|
if err == nil {
|
||
|
|
accepted <- connection
|
||
|
|
}
|
||
|
|
}()
|
||
|
|
client, err := net.Dial("tcp4", listener.Addr().String())
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
defer client.Close()
|
||
|
|
var server net.Conn
|
||
|
|
select {
|
||
|
|
case server = <-accepted:
|
||
|
|
defer server.Close()
|
||
|
|
case <-time.After(time.Second):
|
||
|
|
t.Fatal("proxy did not open the upstream stream")
|
||
|
|
}
|
||
|
|
closed := make(chan struct{})
|
||
|
|
go func() {
|
||
|
|
proxy.close()
|
||
|
|
close(closed)
|
||
|
|
}()
|
||
|
|
select {
|
||
|
|
case <-closed:
|
||
|
|
case <-time.After(time.Second):
|
||
|
|
t.Fatal("proxy shutdown did not close active streams")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMailProxyConnectionLimit(t *testing.T) {
|
||
|
|
upstream := listenTCP(t)
|
||
|
|
listener := listenTCP(t)
|
||
|
|
slots := make(chan struct{}, 1)
|
||
|
|
proxy := newMailProxy(listener, upstream.Addr().String(), nil, make(chan error, 1), slots)
|
||
|
|
go proxy.serve()
|
||
|
|
defer proxy.close()
|
||
|
|
accepted := make(chan net.Conn, 2)
|
||
|
|
go func() {
|
||
|
|
for {
|
||
|
|
connection, err := upstream.Accept()
|
||
|
|
if err != nil {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
accepted <- connection
|
||
|
|
}
|
||
|
|
}()
|
||
|
|
first, err := net.Dial("tcp4", listener.Addr().String())
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
defer first.Close()
|
||
|
|
firstUpstream := waitForAccepted(t, accepted)
|
||
|
|
defer firstUpstream.Close()
|
||
|
|
second, err := net.Dial("tcp4", listener.Addr().String())
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
defer second.Close()
|
||
|
|
if err := second.SetReadDeadline(time.Now().Add(time.Second)); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if _, err := second.Read(make([]byte, 1)); err == nil {
|
||
|
|
t.Fatal("over-limit connection remained open")
|
||
|
|
}
|
||
|
|
select {
|
||
|
|
case connection := <-accepted:
|
||
|
|
_ = connection.Close()
|
||
|
|
t.Fatal("over-limit connection reached the upstream")
|
||
|
|
case <-time.After(100 * time.Millisecond):
|
||
|
|
}
|
||
|
|
_ = first.Close()
|
||
|
|
_ = firstUpstream.Close()
|
||
|
|
deadline := time.Now().Add(time.Second)
|
||
|
|
for len(slots) != 0 && time.Now().Before(deadline) {
|
||
|
|
time.Sleep(time.Millisecond)
|
||
|
|
}
|
||
|
|
if len(slots) != 0 {
|
||
|
|
t.Fatal("connection slot was not released")
|
||
|
|
}
|
||
|
|
third, err := net.Dial("tcp4", listener.Addr().String())
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
defer third.Close()
|
||
|
|
thirdUpstream := waitForAccepted(t, accepted)
|
||
|
|
_ = thirdUpstream.Close()
|
||
|
|
}
|
||
|
|
|
||
|
|
func listenTCP(t *testing.T) *net.TCPListener {
|
||
|
|
t.Helper()
|
||
|
|
listener, err := net.ListenTCP("tcp4", &net.TCPAddr{IP: net.ParseIP("127.0.0.1")})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
t.Cleanup(func() { _ = listener.Close() })
|
||
|
|
return listener
|
||
|
|
}
|
||
|
|
|
||
|
|
func waitForAccepted(t *testing.T, connections <-chan net.Conn) net.Conn {
|
||
|
|
t.Helper()
|
||
|
|
select {
|
||
|
|
case connection := <-connections:
|
||
|
|
return connection
|
||
|
|
case <-time.After(time.Second):
|
||
|
|
t.Fatal("proxy did not open the upstream stream")
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func assertProxyRecord(t *testing.T, record proxyRecord, source, destination *net.TCPAddr) {
|
||
|
|
t.Helper()
|
||
|
|
if len(record.header) != 28 || !bytes.Equal(record.header[:12], proxySignature) || record.header[12] != 0x21 || record.header[13] != 0x11 || binary.BigEndian.Uint16(record.header[14:16]) != 12 {
|
||
|
|
t.Fatalf("invalid PROXY header: %x", record.header)
|
||
|
|
}
|
||
|
|
if !bytes.Equal(record.header[16:20], source.IP.To4()) || !bytes.Equal(record.header[20:24], destination.IP.To4()) || binary.BigEndian.Uint16(record.header[24:26]) != uint16(source.Port) || binary.BigEndian.Uint16(record.header[26:28]) != uint16(destination.Port) {
|
||
|
|
t.Fatalf("unexpected PROXY addresses: %x", record.header)
|
||
|
|
}
|
||
|
|
if string(record.payload) != "ping" {
|
||
|
|
t.Fatalf("payload = %q", record.payload)
|
||
|
|
}
|
||
|
|
if !record.peer.Equal(net.ParseIP("127.0.0.2")) {
|
||
|
|
t.Fatalf("upstream peer = %s", record.peer)
|
||
|
|
}
|
||
|
|
}
|