208 lines
4.8 KiB
Go
208 lines
4.8 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
var proxySignature = []byte("\r\n\r\n\x00\r\nQUIT\n")
|
|
|
|
type mailProxy struct {
|
|
listener net.Listener
|
|
upstream string
|
|
source net.IP
|
|
failures chan<- error
|
|
slots chan struct{}
|
|
mu sync.Mutex
|
|
closed bool
|
|
conns map[net.Conn]struct{}
|
|
workers sync.WaitGroup
|
|
done chan struct{}
|
|
}
|
|
|
|
func newMailProxy(listener net.Listener, upstream string, source net.IP, failures chan<- error, slots chan struct{}) *mailProxy {
|
|
return &mailProxy{listener: listener, upstream: upstream, source: source, failures: failures, slots: slots, conns: make(map[net.Conn]struct{}), done: make(chan struct{})}
|
|
}
|
|
|
|
func (r *relay) startProxies() error {
|
|
if r.config.mailTarget == nil {
|
|
return nil
|
|
}
|
|
slots := make(chan struct{}, mailConnectionLimit)
|
|
for _, port := range r.config.mailTarget.ports {
|
|
listener, err := net.ListenTCP("tcp4", &net.TCPAddr{IP: r.config.mailTarget.address, Port: int(port)})
|
|
if err != nil {
|
|
r.stopProxies()
|
|
return fmt.Errorf("listen on %s:%d: %w", r.config.mailTarget.address, port, err)
|
|
}
|
|
proxy := newMailProxy(listener, net.JoinHostPort(r.config.mailUpstream, strconv.Itoa(int(port))), net.ParseIP(r.config.podIP).To4(), r.proxyFailures, slots)
|
|
r.proxies = append(r.proxies, proxy)
|
|
go proxy.serve()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *relay) stopProxies() {
|
|
for _, proxy := range r.proxies {
|
|
proxy.close()
|
|
}
|
|
r.proxies = nil
|
|
}
|
|
|
|
func (p *mailProxy) serve() {
|
|
defer close(p.done)
|
|
for {
|
|
client, err := p.listener.Accept()
|
|
if err != nil {
|
|
if !p.isClosed() {
|
|
p.report(fmt.Errorf("accept connection: %w", err))
|
|
}
|
|
return
|
|
}
|
|
select {
|
|
case p.slots <- struct{}{}:
|
|
default:
|
|
_ = client.Close()
|
|
continue
|
|
}
|
|
if !p.track(client) {
|
|
<-p.slots
|
|
_ = client.Close()
|
|
return
|
|
}
|
|
p.workers.Add(1)
|
|
go p.forward(client)
|
|
}
|
|
}
|
|
|
|
func (p *mailProxy) forward(client net.Conn) {
|
|
defer p.workers.Done()
|
|
defer func() { <-p.slots }()
|
|
defer p.untrack(client)
|
|
defer client.Close()
|
|
dialer := net.Dialer{Timeout: 10 * time.Second, KeepAlive: 30 * time.Second}
|
|
if p.source != nil {
|
|
dialer.LocalAddr = &net.TCPAddr{IP: p.source}
|
|
}
|
|
server, err := dialer.DialContext(context.Background(), "tcp4", p.upstream)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer server.Close()
|
|
if !p.track(server) {
|
|
return
|
|
}
|
|
defer p.untrack(server)
|
|
if err := writeProxyHeader(server, client.RemoteAddr(), client.LocalAddr()); err != nil {
|
|
return
|
|
}
|
|
clientTCP, clientOK := client.(*net.TCPConn)
|
|
serverTCP, serverOK := server.(*net.TCPConn)
|
|
if !clientOK || !serverOK {
|
|
return
|
|
}
|
|
results := make(chan error, 2)
|
|
go copyStream(results, serverTCP, clientTCP)
|
|
go copyStream(results, clientTCP, serverTCP)
|
|
<-results
|
|
<-results
|
|
}
|
|
|
|
func (p *mailProxy) close() {
|
|
p.mu.Lock()
|
|
if p.closed {
|
|
p.mu.Unlock()
|
|
return
|
|
}
|
|
p.closed = true
|
|
connections := make([]net.Conn, 0, len(p.conns))
|
|
for connection := range p.conns {
|
|
connections = append(connections, connection)
|
|
}
|
|
p.mu.Unlock()
|
|
_ = p.listener.Close()
|
|
<-p.done
|
|
for _, connection := range connections {
|
|
_ = connection.Close()
|
|
}
|
|
p.workers.Wait()
|
|
}
|
|
|
|
func (p *mailProxy) track(connection net.Conn) bool {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
if p.closed {
|
|
return false
|
|
}
|
|
p.conns[connection] = struct{}{}
|
|
return true
|
|
}
|
|
|
|
func (p *mailProxy) untrack(connection net.Conn) {
|
|
p.mu.Lock()
|
|
delete(p.conns, connection)
|
|
p.mu.Unlock()
|
|
}
|
|
|
|
func (p *mailProxy) isClosed() bool {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
return p.closed
|
|
}
|
|
|
|
func (p *mailProxy) report(err error) {
|
|
select {
|
|
case p.failures <- err:
|
|
default:
|
|
}
|
|
}
|
|
|
|
func copyStream(results chan<- error, destination, source *net.TCPConn) {
|
|
_, err := io.Copy(destination, source)
|
|
if closeErr := destination.CloseWrite(); err == nil {
|
|
err = closeErr
|
|
}
|
|
results <- err
|
|
}
|
|
|
|
func writeProxyHeader(writer io.Writer, sourceAddress, destinationAddress net.Addr) error {
|
|
source, sourceOK := sourceAddress.(*net.TCPAddr)
|
|
destination, destinationOK := destinationAddress.(*net.TCPAddr)
|
|
if !sourceOK || !destinationOK {
|
|
return errors.New("PROXY addresses are not TCP")
|
|
}
|
|
sourceIP := source.IP.To4()
|
|
destinationIP := destination.IP.To4()
|
|
if sourceIP == nil || destinationIP == nil || source.Port < 1 || source.Port > 65535 || destination.Port < 1 || destination.Port > 65535 {
|
|
return errors.New("PROXY addresses are not IPv4")
|
|
}
|
|
header := make([]byte, 28)
|
|
copy(header, proxySignature)
|
|
header[12] = 0x21
|
|
header[13] = 0x11
|
|
binary.BigEndian.PutUint16(header[14:16], 12)
|
|
copy(header[16:20], sourceIP)
|
|
copy(header[20:24], destinationIP)
|
|
binary.BigEndian.PutUint16(header[24:26], uint16(source.Port))
|
|
binary.BigEndian.PutUint16(header[26:28], uint16(destination.Port))
|
|
_, err := io.Copy(writer, bytes.NewReader(header))
|
|
return err
|
|
}
|
|
|
|
func drainFailures(failures <-chan error) {
|
|
for {
|
|
select {
|
|
case <-failures:
|
|
default:
|
|
return
|
|
}
|
|
}
|
|
}
|