From 7c2747623547f74cab3bdb565b24edbd81fef086 Mon Sep 17 00:00:00 2001
From: MegaBrutal <code+git@megabrutal.com>
Date: Tue, 20 Nov 2018 21:14:56 +0100
Subject: [PATCH] Bind to user-specified address (BindAddress6)

Bind to user-specified address for outgoing connections.

	modified:   Common.pas
	modified:   MgSMTP.pas
	modified:   Network.pas
	modified:   Relay.pas
---
 Common.pas  |   6 +++
 MgSMTP.pas  |   2 +-
 Network.pas | 105 ++++++++++++++++++++++++++++++++++++++++++++--------
 Relay.pas   |   7 +++-
 4 files changed, 101 insertions(+), 19 deletions(-)

diff --git a/Common.pas b/Common.pas
index 2280c2b..f7a8854 100644
--- a/Common.pas
+++ b/Common.pas
@@ -74,10 +74,13 @@ type
       FTimeOffset: integer;
       FTimeOffsetStr: string;
       FListenAddresses, FListenAddresses6: TStrings;
+      FBindAddress, FBindAddress6: string;
    public
       function GetVersionStr: string;
       property ListenAddresses: TStrings read FListenAddresses;
       property ListenAddresses6: TStrings read FListenAddresses6;
+      property BindAddress: string read FBindAddress;
+      property BindAddress6: string read FBindAddress6;
       property Databytes: longint read FDatabytes;
       {property TimeCorrection: integer read FTimeCorrection;}
       property TimeOffset: integer read FTimeOffset;
@@ -537,6 +540,9 @@ begin
    FListenAddresses6.Delimiter:= ',';
    FListenAddresses6.DelimitedText:= Config.ReadString('Server', 'ListenAddress6', '');
 
+   FBindAddress:=    Config.ReadString('Server', 'BindAddress', '0.0.0.0');
+   FBindAddress6:=   Config.ReadString('Server', 'BindAddress6', '[::]');
+
    FDatabytes:=      Config.ReadInteger('Server', 'Databytes', 1024 * 1024 * 1024);
    FTimeOffset:=     Config.ReadInteger('Server', 'TimeOffset', Config.ReadInteger('Server', 'TimeCorrection', 0) * 100);
    FTimeOffsetStr:=  MakeTimeOffsetStr(FTimeOffset);
diff --git a/MgSMTP.pas b/MgSMTP.pas
index 154ad63..f7a8447 100644
--- a/MgSMTP.pas
+++ b/MgSMTP.pas
@@ -49,7 +49,7 @@ const
      document what bugfix/feature are you testing with the actual build.
      This will be logged to help you differentiate outputs of subsequent
      builds in your logs. If left empty, it won't be added to the logs. }
-   DEVCOMMENT  =  'IPv6 listen addresses (ListenAddress6)';
+   DEVCOMMENT  =  'BindAddress6';
 
 var
 
diff --git a/Network.pas b/Network.pas
index ec602d1..a28ff63 100644
--- a/Network.pas
+++ b/Network.pas
@@ -37,7 +37,7 @@
 unit Network;
 
 interface
-uses Classes, Sockets, SocketUtils, DNSResolve, NetRFC, Common;
+uses Classes, Sockets, SocketUtils, ctypes, DNSResolve, NetRFC, Common;
 
 const
 
@@ -61,15 +61,21 @@ type
    TTCPConnection = class
       constructor Create; overload;
       constructor Create(const HostName: string; Port: word); overload;
-      constructor Create(Socket: socket; const Addr: TSockAddr6); overload;
+      constructor Create(Socket: socket); overload;
       destructor Destroy; override;
    private
       FConnected: boolean;
       FSocket: socket;
       FHostIP: TIPNamePair;
       FSockTimeOut: DWord;
-      SockAddr: TSockAddr6;
+      SrcSockAddr: TSockAddr;
+      SrcSockAddr6: TSockAddr6;
+      DstSockAddr: TSockAddr6;
+   protected
+      function IsNullAddress(SockAddr: PSockAddr): boolean;
+      function BindSrcAddr(Socket: socket; Family: word): cint;
    public
+      function SetBindAddress(Family: word; const HostName: string): boolean;
       function Connect(const HostName: string; Port: word): boolean;
       procedure Disconnect;
       procedure ReverseDNSLookup;
@@ -136,6 +142,11 @@ begin
    FConnected:= false;
    FSocket:= -1;
    FSockTimeOut:= DEF_SOCK_TIMEOUT;
+   FillChar(SrcSockAddr, SizeOf(SrcSockAddr), 0);
+   FillChar(SrcSockAddr6, SizeOf(SrcSockAddr6), 0);
+   FillChar(DstSockAddr, SizeOf(DstSockAddr), 0);
+   SrcSockAddr.sin_family:= AF_INET;
+   SrcSockAddr6.sin6_family:= AF_INET6;
 end;
 
 constructor TTCPConnection.Create(const HostName: string; Port: word);
@@ -145,13 +156,17 @@ begin
    Connect(HostName, Port);
 end;
 
-constructor TTCPConnection.Create(Socket: socket; const Addr: TSockAddr6);
+constructor TTCPConnection.Create(Socket: socket);
 { Use an already connected socket. }
+var ssocklen, dsocklen: TSockLen;
 begin
    inherited Create;
    FSocket:= Socket;
-   SockAddr:= Addr;
-   FHostIP:= TIPNamePair.Create('', IPToStr(@Addr));
+   ssocklen:= SizeOf(SrcSockAddr);
+   dsocklen:= SizeOf(DstSockAddr);
+   fpgetsockname(FSocket, @SrcSockAddr, @ssocklen);
+   fpgetpeername(FSocket, @DstSockAddr, @dsocklen);
+   FHostIP:= TIPNamePair.Create('', IPToStr(@DstSockAddr));
    FConnected:= true;
 end;
 
@@ -184,26 +199,84 @@ begin
 end;
 
 
+function TTCPConnection.IsNullAddress(SockAddr: PSockAddr): boolean;
+begin
+   if SockAddr^.sin_family = AF_INET then
+      Result:= SockAddr^.sin_addr.s_addr = 0
+   else if SockAddr^.sin_family = AF_INET6 then
+      Result:= (PSockAddr6(SockAddr)^.sin6_addr.u6_addr32[0] = 0)
+         and (PSockAddr6(SockAddr)^.sin6_addr.u6_addr32[1] = 0)
+         and (PSockAddr6(SockAddr)^.sin6_addr.u6_addr32[2] = 0)
+         and (PSockAddr6(SockAddr)^.sin6_addr.u6_addr32[3] = 0)
+   else
+      Result:= true;
+end;
+
+function TTCPConnection.BindSrcAddr(Socket: socket; Family: word): cint;
+var SockAddr: PSockAddr; addrlen: size_t;
+begin
+   case Family of
+      AF_INET:
+         begin
+            SockAddr:= @SrcSockAddr;
+            addrlen:= SizeOf(SrcSockAddr);
+         end;
+      AF_INET6:
+         begin
+            SockAddr:= @SrcSockAddr6;
+            addrlen:= SizeOf(SrcSockAddr6);
+         end;
+   end;
+
+   if not IsNullAddress(SockAddr) then
+      Result:= fpBind(Socket, SockAddr, addrlen)
+   else
+      Result:= 0;
+end;
+
+function TTCPConnection.SetBindAddress(Family: word; const HostName: string): boolean;
+var GAIResult: TGAIResult; SockAddr: PSockAddr;
+begin
+   GAIResult:= ResolveHost(HostName, Family);
+   if GAIResult.GAIError = 0 then begin
+      case GAIResult.AddrInfo^.ai_family of
+         AF_INET:    SockAddr:= @SrcSockAddr;
+         AF_INET6:   SockAddr:= @SrcSockAddr6;
+      end;
+      Move(GAIResult.AddrInfo^.ai_addr^, SockAddr^, GAIResult.AddrInfo^.ai_addrlen);
+      FreeHost(GAIResult);
+      Result:= true;
+   end
+   else
+      Result:= false;
+end;
+
 function TTCPConnection.Connect(const HostName: string; Port: word): boolean;
 { Resolves the given hostname, and tries to connect it on the given port. }
 var GAIResult: TGAIResult;
 begin
    GAIResult:= ResolveHost(HostName, AF_UNSPEC);
    if GAIResult.GAIError = 0 then begin
-      Move(GAIResult.AddrInfo^.ai_addr^, SockAddr, GAIResult.AddrInfo^.ai_addrlen);
-      SockAddr.sin6_port:= htons(Port);
+      Move(GAIResult.AddrInfo^.ai_addr^, DstSockAddr, GAIResult.AddrInfo^.ai_addrlen);
+      DstSockAddr.sin6_port:= htons(Port);
 
       { Create socket. }
       FSocket:= fpSocket(GAIResult.AddrInfo^.ai_family, SOCK_STREAM, 0);
 
       if (FSocket <> -1) then begin
 
-         { Try to initiate connection. }
-         FConnected:= fpConnect(FSocket, @SockAddr, GAIResult.AddrInfo^.ai_addrlen) <> -1;
+         if BindSrcAddr(FSocket, GAIResult.AddrInfo^.ai_family) = 0 then begin
+
+            { Try to initiate connection. }
+            FConnected:= fpConnect(FSocket, @DstSockAddr, GAIResult.AddrInfo^.ai_addrlen) <> -1;
+
+            if FConnected then begin
+               FHostIP:= TIPNamePair.Create(HostName, IPToStr(@DstSockAddr));
+               SetSockTimeOut(FSockTimeOut);
+            end
+            else
+               CloseSocket(FSocket);
 
-         if FConnected then begin
-            FHostIP:= TIPNamePair.Create(HostName, IPToStr(@SockAddr));
-            SetSockTimeOut(FSockTimeOut);
          end
          else
             CloseSocket(FSocket);
@@ -229,7 +302,7 @@ procedure TTCPConnection.ReverseDNSLookup;
 var NHostIP: TIPNamePair;
 begin
    if FConnected then begin
-      NHostIP:= TIPNamePair.Create(ResolveIP(PSockAddr(@SockAddr)), FHostIP.IP);
+      NHostIP:= TIPNamePair.Create(ResolveIP(PSockAddr(@DstSockAddr)), FHostIP.IP);
       FHostIP.Free;
       FHostIP:= NHostIP;
    end;
@@ -362,9 +435,9 @@ begin
            connection. }
          case FFeatureRequest of
          NET_TCP_BASIC:
-            TCPConnection:= TTCPConnection.Create(ClientSocket, SockAddr);
+            TCPConnection:= TTCPConnection.Create(ClientSocket);
          NET_TCP_RFCSUPPORT:
-            TCPConnection:= TTCPRFCConnection.Create(ClientSocket, SockAddr);
+            TCPConnection:= TTCPRFCConnection.Create(ClientSocket);
          end;
 
          { Then start a new thread with the connection handler. }
diff --git a/Relay.pas b/Relay.pas
index cf38760..e863023 100644
--- a/Relay.pas
+++ b/Relay.pas
@@ -1,6 +1,6 @@
 {
    MegaBrutal's SMTP Server (MgSMTP)
-   Copyright (C) 2010-2015 MegaBrutal
+   Copyright (C) 2010-2018 MegaBrutal
 
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
@@ -331,7 +331,10 @@ var MXList: TStrings; i: integer;
 begin
    MXList:= GetCorrectMXRecordList(RelayServerName);
    if MXList.Count >= 1 then begin
-      TCP:= TTCPRFCConnection.Create(MXList.Strings[0], RelayServerPort);
+      TCP:= TTCPRFCConnection.Create;
+      TCP.SetBindAddress(AF_INET,  MainServerConfig.BindAddress);
+      TCP.SetBindAddress(AF_INET6, MainServerConfig.BindAddress6);
+      TCP.Connect(MXList.Strings[0], RelayServerPort);
       TCP.SetSockTimeOut(DEF_SOCK_TIMEOUT);
       i:= 1;
       while (not TCP.Connected) and (i < MXList.Count) do begin
-- 
2.34.1