[solved] CIDR in acl_check_connect

Postfix, QMail, Sendmail, Dovecot, Cyrus, Courier, Anti-Spam
User avatar
daemotron
Administrator
Administrator
Posts: 2800
Joined: 2004-01-21 17:44

[solved] CIDR in acl_check_connect

Post by daemotron » 2010-02-27 23:22

Moin,

ich habe eine ACL gebastelt, mit der ich einzelne IP-Adressen sperren kann, indem ich sie in eine MySQL-Tabelle schreibe (ist dreckiger Workaround, aber die MySQL-Tabelle kann ich zur Not via phpMyAdmin bearbeiten - mit einer pf Tabelle geht das leider nicht).

Die ACL sieht so aus (und funktioniert auch):

Code: Select all

acl_check_connect:
   deny  message         = We do not accept mails from spammers.
         log_message     = Rejected connection from ${sender_host_address}: This host is blacklisted.
         condition       = ${lookup mysql{ SELECT DISTINCT `host` from `blacklist` WHERE `host` = '${quote_mysql:$sender_host_address}' }{true}{false}}
   accept


Ich würde die Regel nun gerne so anpassen, dass ich in der Tabelle nicht nur einzelne IPs, sondern auch Subnetze (in CIDR-Notation) hinterlegen kann. Gegen einen CIDR kann ich natürlich nicht $sender_host_address matchen. Hat jemand eine Idee, wie man das anpacken könnte? Ggf. reicht mir schon ein Stichwort, aber bei Google habe ich zu Exim ACLs und CIDR bisher nicht das gewünschte gefunden.
Last edited by daemotron on 2010-02-28 11:36, edited 1 time in total.
“Some humans would do anything to see if it was possible to do it. If you put a large switch in some cave somewhere, with a sign on it saying 'End-of-the-World Switch. PLEASE DO NOT TOUCH', the paint wouldn't even have time to dry.” — Terry Pratchett, Thief of Time

Roger Wilco
Administrator
Administrator
Posts: 6001
Joined: 2004-05-23 12:53

Re: CIDR in acl_check_connect

Post by Roger Wilco » 2010-02-28 07:34

jfreund wrote:Ich würde die Regel nun gerne so anpassen, dass ich in der Tabelle nicht nur einzelne IPs, sondern auch Subnetze (in CIDR-Notation) hinterlegen kann. Gegen einen CIDR kann ich natürlich nicht $sender_host_address matchen. Hat jemand eine Idee, wie man das anpacken könnte?

Im Prinzip musst du nur die Subnetzberechnung in SQL machen.

Code: Select all

-- Beispiel IP-Adresse gegen 10.0.0.0/8 matchen

-- Integer-Repraesentation der Netzwerkadresse
mysql> select INET_ATON('10.0.0.0');
+-----------------------+
| INET_ATON('10.0.0.0') |
+-----------------------+
|             167772160 |
+-----------------------+
1 row in set (0.00 sec)

-- Das ganze binaer...
mysql> select BIN(INET_ATON('10.0.0.0'));
+------------------------------+
| BIN(INET_ATON('10.0.0.0'))   |
+------------------------------+
| 1010000000000000000000000000 |
+------------------------------+
1 row in set (0.00 sec)

-- ... und auf 32 Bits padden.
mysql> select LPAD(BIN(INET_ATON('10.0.0.0')), 32, 0);
+-----------------------------------------+
| LPAD(BIN(INET_ATON('10.0.0.0')), 32, 0) |
+-----------------------------------------+
| 00001010000000000000000000000000        |
+-----------------------------------------+
1 row in set (0.00 sec)

-- Dann die Anzahl der Bits extrahieren, die fuer uns interessant sind (in diesem Fall die ersten 8 Bits bei 10.0.0.0/8)...
mysql> select SUBSTRING(LPAD(BIN(INET_ATON('10.0.0.0')), 32, 0), 1, 8);
+----------------------------------------------------------+
| SUBSTRING(LPAD(BIN(INET_ATON('10.0.0.0')), 32, 0), 1, 8) |
+----------------------------------------------------------+
| 00001010                                                 |
+----------------------------------------------------------+
1 row in set (0.00 sec)

-- ... und die IP-Adresse mit dem Subnetz aus der Datenbank abgleichen. Wiederum interessieren uns nur die ersten 8 Bits.
mysql> select 1 FROM `blacklist_db` WHERE SUBSTRING(LPAD(BIN(INET_ATON('10.20.30.40')), 32, 0), 1, 8) = '00001010';
+---+
| 1 |
+---+
| 1 |
+---+
1 row in set (0.00 sec)

mysql> select 1 FROM `blacklist_db` WHERE SUBSTRING(LPAD(BIN(INET_ATON('10.20.30.40')), 32, 0), 1, 8) = SUBSTRING(LPAD(BIN(INET_ATON('10.0.0.0')), 32, 0), 1, 8);
+---+
| 1 |
+---+
| 1 |
+---+
1 row in set (0.00 sec)

-- Wenn die IP-Adresse nicht in dem Netz ist, wird kein Ergebnis zurueckgeliefert
mysql> select 1 FROM `blacklist_db` WHERE SUBSTRING(LPAD(BIN(INET_ATON('11.20.30.40')), 32, 0), 1, 8) = SUBSTRING(LPAD(BIN(INET_ATON('10.0.0.0')), 32, 0), 1, 8);
Empty set (0.00 sec)


Die finale Abfrage musst du noch an die Felder deiner Datenbank anpassen, aber ansonsten sollte das so funktionieren.

Du musst also, wenn du gegen ein Subnetz testen willst, nur die Netzmaske (gegen die Schreibarbeit vielleicht schon in der richtigen Notation) in der Datenbank hinterlegen und die IP-Adresse ein bisschen durch den Fleischwolf drehen.

matzewe01 wrote:Ich kenne nun die CIDR Notation nicht

Jetzt mach mich nicht schwach... ;)


MOD: Das ist eher ein MySQL- als ein Exim-Problem. ;)
Last edited by Roger Wilco on 2010-02-28 07:34, edited 1 time in total.

User avatar
daemotron
Administrator
Administrator
Posts: 2800
Joined: 2004-01-21 17:44

Re: CIDR in acl_check_connect

Post by daemotron » 2010-02-28 08:55

Roger Wilco wrote:MOD: Das ist eher ein MySQL- als ein Exim-Problem. ;)

Danke, das ist ein guter Denkanstoß. Hab die ganze Zeit in der falschen Richtung gesucht (CIDR-Matching in Exim :roll: ) Muss wohl doch mal meine SQL-Kenntnisse aufpolieren :D


Edith sagt, ich sollte wohl mal die genaue Lösung posten:

Zuerst mal eine geeignete Tabelle in der Datenbank anlegen:

Code: Select all

create table `blacklist` (
   `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
   `ip` VARCHAR(16) NOT NULL,
   `mask` TINYINT UNSIGNED NOT NULL,
   `entry_created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
   PRIMARY KEY(`id`),
   UNIQUE KEY `cidr` (`ip`, `mask`)
);


Mit folgenden Statements lässt sich überprüfen, dass tatsächlich Subnetz-Matches erkannt werden:

Code: Select all

-- erst mal ein Subnetz eintragen
INSERT INTO `blacklist` (`ip`, `mask`) VALUES ('192.168.0.0', 16);

-- Positiv-Beispiel
SELECT 1 FROM `blacklist` WHERE
   SUBSTRING(LPAD(BIN(INET_ATON('192.168.1.2')), 32, 0), 1, `blacklist`.`mask`)
   = SUBSTRING(LPAD(BIN(INET_ATON(`blacklist`.`ip`)), 32, 0), 1, `blacklist`.`mask`);

-- Negativ-Beispiel
SELECT 1 FROM `blacklist` WHERE
   SUBSTRING(LPAD(BIN(INET_ATON('10.0.0.1')), 32, 0), 1, `blacklist`.`mask`)
   = SUBSTRING(LPAD(BIN(INET_ATON(`blacklist`.`ip`)), 32, 0), 1, `blacklist`.`mask`);


Die ACL für Exim sieht dementsprechend so aus:

Code: Select all

acl_check_connect:
  deny  message      = We do not accept mails from spammers.
        log_message  = Rejected connection from ${sender_host_address}: This host is blacklisted.
        condition    = ${lookup mysql{ SELECT 1 FROM `blacklist` WHERE SUBSTRING(LPAD(BIN(INET_ATON('${quote_mysql:$sender_host_address}')), 32, 0), 1, `blacklist`.`mask`) = SUBSTRING(LPAD(BIN(INET_ATON(`blacklist`.`ip`)), 32, 0), 1, `blacklist`.`mask`); }{true}{false}}
  accept

Die condition gehört natürlich in eine Zeile. Übrigens: wenn man {true} und {false} vertauscht, bekommt man eine Whitelist. Die sollte man vielleicht aber nicht in acl_check_connect einsetzen - zumindest nicht für einen öffentlich erreichbaren Mailserver :wink:
Last edited by daemotron on 2010-02-28 11:35, edited 1 time in total.
“Some humans would do anything to see if it was possible to do it. If you put a large switch in some cave somewhere, with a sign on it saying 'End-of-the-World Switch. PLEASE DO NOT TOUCH', the paint wouldn't even have time to dry.” — Terry Pratchett, Thief of Time

Roger Wilco
Administrator
Administrator
Posts: 6001
Joined: 2004-05-23 12:53

Re: CIDR in acl_check_connect

Post by Roger Wilco » 2010-02-28 12:01

matzewe01 wrote:Bei ggf. sehr stark anwachsenden Datenmengen, und die können in dem Umfeld u.U. sehr gross werden, würdest Du mit Deinen Where Bedingungen immer Full Table Scans erzwingen.

Ich wuerde einfach das Ergebnis der Konvertierung der Subnetzmaske als Feld (mit maximal 32 Zeichen fuer IPv4 bzw. 128 Zeichen fuer IPv6) in der Datenbank hinterlegen. Dadurch spart man sich die (einmalige) Tipparbeit bei der Abfrage und Indizes greifen wieder, was einen Full Table Scan vermeidet.

Roger Wilco
Administrator
Administrator
Posts: 6001
Joined: 2004-05-23 12:53

Re: CIDR in acl_check_connect

Post by Roger Wilco » 2010-02-28 12:08

matzewe01 wrote:Aber wie unterscheidest Du dann, ob der Datensatz als CIDR Information oder einzelne IP Adresse ein getrudelt ist?

Einfach an der Netzmaske /32. Diese Information ist im Schema von jfreund bereits in `blacklist`.`mask` hinterlegt.

Roger Wilco
Administrator
Administrator
Posts: 6001
Joined: 2004-05-23 12:53

Re: [solved] CIDR in acl_check_connect

Post by Roger Wilco » 2010-02-28 12:25

matzewe01 wrote:Ich würde allerdings die IP ggf. in Blöcke unterteilen:

IPv: Block 1-4 jeweils int 3.

Das ist IMHO zu feingranular. Die CIDR-Notation ist schon ein ganz guter Kompromiss aus kompakter und eindeutiger Darstellung. Und so ganz unterschaetzen muss man MySQL (bzw. allgemein relationale DBMSe) auch nicht, zumal "richtige" RDBMSe bereits entsprechende Datentypen mitbringen. ;)

matzewe01 wrote:Bei allen theoretischen IPv4 Adressen, liegt man schnell bei mehr als 200 GB Daten.

Falls du tatsaechlich das gesamte IPv4-Internet blacklisten willst (eben alle IPv4-Adressen), kannst du das mit 0.0.0.0/0 mit nur 37 Bit Platzbedarf (32 Bit IP-Adresse, 5 Bit fuer die Maske) haben. ;)

User avatar
daemotron
Administrator
Administrator
Posts: 2800
Joined: 2004-01-21 17:44

Re: CIDR in acl_check_connect

Post by daemotron » 2010-02-28 12:44

matzewe01 wrote:Ich weiss jetzt nicht, was JFreund explizit mit der Lösung vor hat und wie viele Zugriffe sich da ergeben.
Aber damit lässt sich die Mysql DB ziemlich gut DDOSen.
Ich würde nochmals übers Tabellen und Datendesign nach denken.


OK, ich präzisiere das mal. Die Blacklist ist eine "Verlegenheitslösung", die benutzt wird, wenn aus einem (externen) Subnetz so viel Spam eingekippt wird, dass die Bayes-Filter abrauchen. In so einem Fall "verbanne" ich das betreffende Subnetz, im Idealfall so lange, bis auf meine Abuse-Meldung reagiert wurde. Die Tabelle hat nur eine handvoll Datensätze (derzeit ca. 15 Einträge), deswegen würde sich IMO indexed search vs. full table scan nicht mal besonders stark bemerkbar machen. Allerdings gefällt auch mir nicht, dass Handler_read_rnd_next extrem nach oben schießt - wirklich elegant ist das nicht, und skalieren würde es auch miserabel (und für IPv6 gleich gar nicht funktionieren, weil INET_ATON nur IPv4 umrechnen kann).

Ebenfalls problematisch ist, dass die Abfrage tatsächlich bei jedem Connect zum Mailserver bemüht wird - dem gegenüber steht aber, dass MySQL das Ergebnis der Abfrage cached, und solange da immer dieselben IPs rumhämmern ist die Cache Hit Rate an der Stelle gar nicht mal schlecht (nach 5 Minuten schon bei >60% Hit Rate).

Ich werde mal ein bisschen herumfrickeln, um das Problem mit den Full Table Scans zu reduzieren. Allein die Speicherung der Binär-Notation des Subnetzes in einem indizierten Feld wird nicht allzu viel bringen, da ich ja trotzdem die zu überprüfende IP mit dem Wert aus dem mask-Feld umrechnen muss. Dazu müssen in jedem Fall alle Datensätze sequenziell durchgearbeitet werden. Das wäre nur anders, wenn ich eine konstante Mask verwenden würde. Dann könnte ich mit der konstanten Mask einmal umrechnen und dann ein normales SELECT auf ein inidziertes Feld loslassen - oder sehe ich das falsch?
“Some humans would do anything to see if it was possible to do it. If you put a large switch in some cave somewhere, with a sign on it saying 'End-of-the-World Switch. PLEASE DO NOT TOUCH', the paint wouldn't even have time to dry.” — Terry Pratchett, Thief of Time

User avatar
daemotron
Administrator
Administrator
Posts: 2800
Joined: 2004-01-21 17:44

Re: [solved] CIDR in acl_check_connect

Post by daemotron » 2010-02-28 13:52

matzewe01 wrote:Wenn es aber wirklich nur so wenige Datensätze sind, dann würde ich das schlicht weg in eine Datei schreiben mit der jeweils gewünschten CIDR Notation, oder so, wie es für dich am besten passt.

Das geht um ein vielfaches schneller als damit die DB zu bemühen.

Wie ich eingangs schon gesagt habe - am elegantesten wäre es in der Tat, die Liste in einer Datei abzulegen, die von pf als Table Source verwendet wird. Dann würden Spam-Einkipper schon vor dem MTA auf Layer 3 gestoppt. Die Sache mit der DB ist nur eine Verlegenheitslösung, weil ich nicht zu jeder Tages- und Nachtzeit Zugriff auf die Konsole habe, auf phpMyAdmin hingegen schon. Und pf über eine Weboberfläche zu füttern halte ich für keine gute Idee - nachher hackt das jemand und sperrt mich aus...
“Some humans would do anything to see if it was possible to do it. If you put a large switch in some cave somewhere, with a sign on it saying 'End-of-the-World Switch. PLEASE DO NOT TOUCH', the paint wouldn't even have time to dry.” — Terry Pratchett, Thief of Time