gps - greylist policy service for postfix

Greylisting is a way of reducing spam coming into users' mailboxes on the mail server. gps (it soon will be called greylist) is a fast and secure implementation of a greylist policy service for the postfix mail server.

gps is no longer actively developed. Use the Greylist daemon instead. The greylist daemon is completely compatible to gps. The same database schema is used and any configuration that was possible with gps can be done with the greylist daemon (but is slightly more complicated).

1.007

Author:
Michael Moritz mimo/at/restoel.net
Last modified on
July 10 2009

UPDATE 6/4/2011:There is now a Belorussian translation of this document by Bohdan Zograf. Many thanks!

Sourceforge Page | Introduction | Installation | Configuration | Running | Whitelisting | Database Maintenance | Todo and known bugs | Changelog | Credits | Download | Discussion & Support | The gps Forum | Links | Class hierarchy and source documentation Some of the above links may not work at the moment as gps has moved to a new web site

Introduction

gps, firstly, is an implementation of a greylist policy service for postfix. Greylisting is a concept to reduce the amount of UCE ('spam') by technical means. Tests on production systems show that greylisting is hugely effective against spam. Read more about greylisting on http://www.greylisting.org and http://projects.puremagic.com/greylisting/whitepaper.html

Secondly, gps takes greylisting one step further starting with version 1.0. Based on the experience of using greylisting in a production environment, gps comes with features that hugely reduce the problems of the original greylisting concept. These improvements make gps' greylisting usable for ISPs and big mail system setups.

gps' main features are:

  • Uses a database backend through DBI
  • Allows sharing one database between all mail servers
  • Supports various database types, tested with mysql, postgresql, sqlite
  • Written in C/C++, using STL and libstdc++
  • Good compromise between speed and safety
  • Logging via syslog
  • Whitelisting by client network addresses, recipient and sender email address
  • Pattern matching based whitelisting (regex): wl_pattern
  • Database maintenance through a customisable perl script run by crond
  • Supports weak greylisting
  • Unique method of reverse weak/light greylisting to correctly identify mail from mail relays
  • Confirmed to run on Linux and FreeBSD so far

Project Status

  • Version 1.x
    • Stable, more than two years of running on production mail system with two mail servers
  • Version 0.x
    • Tested on Debian, RedHat 9 and 7.2
    • Tested on RedHat 9 and 7.2
    • Ongoing test on production mail system with two mailservers sharing one MySQL database

Installation

To build gps from source the following packages are required:

To build gps unpack the source tar ball (not if using SVN) and run configure and make. Since gps is under development you may have to do:

tar xvfz gps-<version>.tar.gz
[OR] 
tar xvfz gps-<version>.tar.gz
cd gps-X.X (or cd release-<version>)
make -f Makefile.cvs
./configure
make
make install
Alternatively, it is often possible to build gps manually by running (works on debian):
g++ -s -o gps configreader.cpp db.cpp main.cpp read.cpp cfg.cpp dbdefs.cpp wlcacheddb.cpp signals.cpp -ldbi -ldl 

Note:
If you get stuck with installing gps post your problem in the gps Forum

Configuration

First create an empty database for greylisting. How to do this depends on the database backend.

Example for mysql (this does not use a password):

# mysql -p
> CREATE DATABASE greylist;
> GRANT ALL ON greylist TO 'greylist' IDENTIFIED BY 'secret';
> BYE
Note:
etc/gps.pgsql.conf in the package contains a step by step example on how to do this in postgresql.
gps will create its Triplets table (and other tables) when it is run in mode=init.

Add gps to your master.cf and main.cf files as described in the postfix documentation under greylisting (taken from http://www.postfix.org/SMTPD_POLICY_README.html):

/etc/postfix/master.cf:
    policy  unix  -       n       n       -       -       spawn
      user=nobody argv=/usr/local/bin/gps /usr/local/etc/gps.conf

/etc/postfix/main.cf:
    smtpd_recipient_restrictions =
        ... 
        reject_unauth_destination 
        check_policy_service unix:private/policy 
        ...
    policy_time_limit = 3600

Syntax

gps [-v] configfile
Parameters:
-v enables verbose log messages
configfile your config file including path

Configuration File

The following options are used in the configuration file.
Note:
the keys and values are case sensitive.
ParameterPossible values or value range
(default in bold)
DescriptionDepends onVersion
modenormal | init | weak | reverseSets the greylisting mode   
weakbytes0 - 4 (3)Number of significant bytes of client IP addressmode=weak(|reverse)0.92
dbtypemysql | sqlite | pgsqlDatabase type   
db_hosthostname or IP addressDatabase serverdbtype  
db_usernameusernameDatabase user namedbtype  
db_passwordpasswordDatabase passworddbtype  
db_dbnamedatabase nameDatabase namedbtype  
db_portport numberDatabase portdbtype=pgsql 0.9
db_pgsql_optionsPostgres optionsPostgres optionsdbtype=pgsql 0.9
db_pgsql_tty/dev/ttyX (/dev/null)Postgres loggingdbtype=pgsql 0.9
db_sqlite_dbdirpath (permissions!)SQLite Database pathdbtype=sqlite 0.9
timeoutseconds (3600=1 hour)Greylisting timeout(mode=init)  
wl_networkoff | db | dbcached (off)Network whitelisting mode 0.8
wl_recipientoff | db | dbcached (off)Recipient whitelisting mode 0.8
wl_senderoff | db | dbcached (off)Sender whitelisting mode 0.8
wl_patternoff | db | dbcached (off)Pattern matching whitelisting mode 0.91

mode

mode tells gps in which mode to run. Default is init
  • init Creates any database table(s) that dont exist. Will always return "dunno" but add records to the database. It can therefore be used to gather a sufficient number of records for the database before switching over to the reverse, normal or weak mode which actually perform the greylisting. Note that this causes more SQL queries as it checks whether the greylist tables exist. This should only be used for initialising the database and testing gps.
    Note:
    This will not stop any mail.

    In the 1.x version init mode should only be used for creating the database tables and not to fill the triplet table with data. The triplets that this mode writes to the database are inconsistent with running gps in mode reverse later. This results in certain mail being let through without any checking. If this is the case there are messages about exceptions in the mail log. The solution is to remove all entries that have client addresses beginning with numbers from the triplet table. Something like this will do the job:

    > DELETE FROM triplet WHERE client_address RLIKE '1';
    
    > DELETE FROM triplet WHERE client_address RLIKE '2';
    ...
    
  • reverse Name based greylisting: instead of checking the client IP address of the triplet this resolves the IP address and uses only a significant part of the host name for checking whether a triplet has already been checked. (E.g. Given the IP address 1.2.3.4 resolves to mail-server255.someisp.com gps checks whether a triplet (someisp.com,sender,recipient) already exists. In normal mode it would check (1.2.3.4,sender,recipient) thus rejecting mail if it came from a different machine on the same relay) This mode is useful for ISPs as it reduces the need to whitelist and therefore complaints from users about not getting mail. At the same time it is still effective in stopping spam. In case the name resolution fails this is logged and weak greylisting based on the IP address is done instead.
    Note:
    This is supported from version 1.0 on and is the recommended mode to use
  • normal Normal greylisting mode: gps checks the triplets (client IP address,sender,recipient) against the database and blocks mail until the timeout is reached for the first mail. Subsequent mails with the same triplet are let through immediately.
  • weak (Starting from version 0.7) Weak greylisting. In brief this means ignore the last byte of client IP addresses. It is useful if you get stuck with sender mail servers that use a block of network addresses on the same subnet to send mail. Using weak mode results in significantly higher cpu load. It is therefore recommended to use a combination of Whitelisting methods (for version 0.x).
    Note:
    From version 1.0 on gps uses a different way of storing and looking up client IP addresses. This reduces CPU load. A new configuration setting is added:
    weakbytes sets the number of significant bytes of the client address in weak greylisting mode. The default is 3.

timeout

timeout The greylisting whitepaper suggests a timeout of 3600 seconds (1 hour) before a new triplet of sender, recipient, client address should be allowed through the greylisting system. Reducing the timeout will keep users happy and is still very effective (e.g. 60 seconds). Default is 3600

dbtype

dbtype sets the database type to use. This must be set to the same name libdbi expects. Currently, libdi-drivers support mysql, pgsql (version 0.9+), sqlite (version 0.9+), msql (?), oracle (?). gps will exit and log a list of available drivers if the specified driver is not installed (comes in handy for checking libdbi installation). Default value is mysql

db_<db parameter>

db_<db parameter> This is the list of paramters to be passed on to libdbi to make the database connection. The db parameters depend on the driver. See the example configuration file below and the included gps.conf, gps.sqlite.conf and gps.pgsql.conf for examples of how to use the different database backends. Example db parameters for dbtype=mysql:
db_host=localhost
db_username=gps
db_password=secret
db_dbname=greylist

Whitelisting

A good greylisting implementation should include several ways of whitelisting. Many mail systems do not conform to the SMTP specification, some big ISPs use multiple mail servers on the same subnet. In the worst case mails get bounced back. Weak greylisting (sometimes called light greylisting) is one way to attack this problem, whitelisting is better in CPU load and results in better spam reduction. In the 1.x series gps uses a better approach to this problem. Using the mode reverse solves the issues with mail relays and thus reduces the need to whitelist.

The following whitelisting options are provided by gps. The can be used in any combination. If you try to optimise your configuration bear in mind that the whitelisting tables get processed before the triplets table.

Whitelisting Database Modes (version 0.8+)

All whitelisting modules can use different ways of storing data and looking up entries. The mode to use is set in the configuration file.
wl_<module>=<mode>

Supported modes are:
Parameters:
off Is the default. This whitelisting module is not used.
db (version 0.7b+) The whitelisting data is stored in a table with the name of the whitelisting module. If gps is run in mode=init it will check if the table exists and create it if necessary. Setting a module to db makes gps check evry triplet against the whitelisting module's table before checking the main triplets table. Therefore, for every whitelisting modules enabled one more SQL query is generated.
dbcached (version 0.8+) When gps is started it reads the module's whitelisting table and creates a memory cache of it which it uses to do subsequent lookups. This uses more memory than db and results in longer startup times, but means fewer SQL queries, and is - once initialised - much faster than db.

wl_network

wl_network (version 0.7b+) This sets the network whitelisting mode. If it is set to wl_network=db it will check the table network prior to everything else whether the client address network block has been whitelisted. In order to turn it off use wl_network=off. Default off

Example of adding a whitelisting entry in mysql

> use greylist;
> insert into network values ('192.168.0.','my home network');
> bye (or CTRL+D)
Note:
The last dot of the netblock is mandatory!

wl_recipient, wl_sender

wl_recipient (version 0.8+) This sets the recipient (or sender) whitelisting mode. If it is set to wl_recipient=db it will check the table recipient prior to everything else whether the recipient address has been whitelisted. In order to turn it off use wl_recipient=off. Default off

Example of adding a whitelisting entry in mysql

> use greylist;
> insert into recipient values ('bla@mydomain.com','this user wants his spam');
> bye (or CTRL+D)

wl_pattern

wl_pattern (version 0.91+) allows whitelisting based on regular expression matching.

The regular expressions in wl_pattern can, theoretically, be used to replace any of the other whitelisting modules. Furthermore, it can be used to implement complex whitelisting rules combining several conditions. Nevertheless, it should only be used if none of the other modules suit the task. It is much slower by itself and also because all its patterns will be tested against any incoming triplet. The other modules use database or string map based lookups. If wl_pattern has to be used this should be done by setting it to wl_pattern=dbcached thus reducing the number of database queries.

gps builds a text that expressions can be matched against for advanced whitelisting solutions. The format of the gps internal representation is:

s=someuser@yahoo.com
r=someuser@mydomain.org
c=216.145.54.171
h=mrout1.yahoo.com
If a pattern contains a h= line gps does a reverse name lookup. This makes gps slower. If no patterns contain h= the reverse lookup is skipped (It's a good idea to run nscd in a reverse lookup situation). From version 1.x on gps also does the lookup if it is run in mode reverse. In this case only the reverse name lookup is already performed thus there is no difference in the performance. Tests also show that the effect reverse lookups is not as bad as orginially assumed.

In the above example the IP address resolves to one of Yahoo's servers. This pattern uses reverse name lookup and matches the example:

> insert into pattern values(".+^h=.*yahoo\.com.+$","yahoo");
Another example: this whitelists one of your mail domains completely
> insert into pattern values(".+^r=.*@someorg\.org.+$","someorg want all spam");
A more complex example for a common situation. A user has problems with receiving mail from someone particular. In this example we even know the sender's mail server's IP address -- well at least the first byte:
^s=user.+^r=myuser@mydomain.+^c=210

If you wanted to specify the users full address it would look like this
^s=user.+^r=myuser@mydomain\.org.+^c=210
Note:
the .+ after the org in the example is still required!
Since s=user is at the beginning do not use the leading .+ before the anchor ^
^s=sender@example\.com.+$

weakbytes

weakbytes sets the number of significant bytes of the client address in weak greylisting mode. The default is 3.

Example configuration file:

mode=reverse
dbtype=mysql 
db_host=localhost
db_username=gps
db_password=secret
db_dbname=greylist
timeout=60
wl_recipient=dbcached
wl_network=db
wl_sender=off
wl_pattern=dbcached

To test gps and your configuration use the following command. Configuration errors will be logged to syslogd (facility mail). Also see Running.

./src/gps -v etc/gps.conf < tests/testinput4.txt
If everything is installed correctly a couple of "action=permit_if_defer" should be printed. If this is not the case check the mail log for errors. If you are stuck at this point post your configuration file, the relevant section of the mail log, and the versions of gps, libdbi, libdb-drivers in the gps Forum.

Now wait for the number of seconds specified in timeout and run the same line again. It should return "action=dunno" lines. If it does gps is ready.

Note:
If you plan to use gps in reverse mode (strongly recommended) then you must now clear out the triplet table. E.g.
> TRUNCATE TABLE `triplet`;

Again, check the log and post in the Forum if something goes wrong.

Example configuration files for postgres and SQLite are in the etc/ folder after unpacking gps. The gps.pgsql.conf contains step by step instruction on how to install and configure postgres on debian and how to create the greylist database and user.

Postgres outputs information on table creation and an error on creating the secondary index when run in mode=init. Nevertheless, it is useable after this.

Note:
If you get stuck with configuring gps post your problem in the gps Forum

Running

For running gps the requirements are:

gps logs its actions to the syslog mail facility. The output from a testrun is shown below:

Note:
Running gps in verbose mode (-v switch) generates a lot of log output and is only recommended for initialising and troubleshooting.
 mail gps[2225]: started (ver.: 0.8 built: Sep 14 2004 18:35:14)
 mail gps[2225]: reading config: /etc/gps.conf
 mail gps[2225]: config: prefix:  key: mode value=normal
 mail gps[2225]: config: prefix: db key: host value=localhost
 mail gps[2225]: config: prefix: db key: username value=greylist
 mail gps[2225]: config: prefix: db key: password value=
 mail gps[2225]: config: prefix: db key: dbname value=greylist
 mail gps[2225]: config: prefix:  key: timeout value=60
 mail gps[2225]: connecting to DB, using driver mysql
 mail gps[2225]: setting DB option: dbname to: greylist
 mail gps[2225]: setting DB option: host to: localhost
 mail gps[2225]: setting DB option: password to: 
 mail gps[2225]: setting DB option: username to: root
 mail gps[2225]: connected to DB
 mail gps[2225]: ok: 'foobar.tld' -> 'barfoo.tld', '1.2.3.4' (3, 152 secs)
 mail gps[2225]: action=dunno
 mail gps[2225]: new: 'foo@blabla.org' -> 'blabla@foo.org', '192.168.0.1'
 mail gps[2225]: action=defer_if_permit Service is unavailable
 mail gps[2225]: wait: 'foo@blabla.org' -> 'blabla@foo.org', '192.168.0.1' (0, 34 secs)
 mail gps[2225]: action=defer_if_permit Service is unavailable
 mail gps[2225]: disconnecting from DB

While gps is running it logs information about the records it receives from postfix. The typical path of a non-spam record is
  1. new: sender -> recipient, client_address|client_name
  2. wait: sender -> recipient, client_address|client_name (count, time_difference first seen)
  3. ok: sender -> recipient, client_address|client_name (count, time_difference last seen)

Parameters:
sender the sender's address
recipient the recipient's address
client_address the client's address
client_name the significant part of the resolved client name when run in mode reverse
count number of times triplet has been passed from postfix
time_difference interval between now and the record's time
gps also logs information about whitelisting. The format of these messages is:
 mail gps[18838]: wl recipient: 'foobar.tld' -> 'bla@mydomain.com', '192.168.0.254': this user wants his spam
 
 mail gps[18452]: wl network: 'foobar.tld' -> 'bla@someorg.org', '192.168.0.254': my home network

Note:
If you have questions about running gps post in the gps Forum

Database Maintenance

The greylisting approach requires a level of database maintenance. This implementation uses an example perl script for database maintenance. This can be run from cron.
gps-maintain.pl [-v] [-delete] -eq|-lt count -age seconds configfile
A typical usage example would be:
/usr/local/bin/gps-maintain.pl -delete -eq 0 -age 18000 /usr/local/etc/gps.conf

This could be run hourly to delete entries that have not been received again within 5 hours.
/usr/local/bin/gps-maintain.pl -delete -age 3110400 /usr/local/etc/gps.conf
This could be run daily to delete entries that are older than 35 days.

Todo and known bugs

  • A couple of know SQLite and postgresql problems especially on table creation. Workaround at the moment: create them manually, check the forums for details
  • A problem with reverse DNS: a couple of companies have decided to use more than one cannonical name for their servers. Reverse greylisting has (non-critical) problems with that
  • Fix Solaris+SQLite problems: one user reports that gps reports many exceptions and spam is going through his machine as a result (help needed)
  • Addition to gps-maintain.pl for whitelisting and gathering statistics
  • Build Debian package (help needed)
  • Add a two-servers-one-database configuration howto
  • Fix postgresql errors on create index. Postgres thinks sender(15),recipient(15) is a call to a function.

Note:
If you think you found a bug or if you have improved gps post in the gps Forum

Credits

  • Thanks to Cedric for suggesting the new reverse mode.
  • Thanks to Marek Tichy for proposing this project and helping implement it and thanks to Cedric Knight for all the feedback.
  • Thanks to the person whose name I have to look up in my E-mail for feedback on the SQL implementation and testing it on Postgres.
  • Credits to Michael Hubbard for porting gps to FreeBSD and rewriting gps-maintain.pl

Changelog

  • 10/07/2009 -- version 1.007, r41, bugfixes, upgrade recommended (Cedric)
    • fixes: use weak, not 'unknown' rDNS
    • fix redundant gethostbyaddr() calls
    • fix triplet table creation syntax
  • 3/04/2007-24/04/2007 -- version 1.005, bugfix, upgrade recommended
    • Moved project to Sourceforge and SVN Sourceforge Page
    • Fix for bug when two machines run at different times and timestamps are in the past
    • Bugfixes for policy service timeout errors
    • Support for clean shutdown on signals
    • use reverse dns provided by postfix instead of resolving - still resolves if necessary
  • 2/03/2005 -- version 1.004, bugfix, upgrade recommended
    • Fix for bug with email addresses, client names, etc. that contain %s or any other printf/syslog/dbi_conquery-type parsed expression that resulted in gps segfaulting and mail being let through without checking (Bug reported by Jamie L. Penman-Smithson)
    • New gps-maintain.pl script from Michael Hubbard
  • 15/02/2005 -- version 1.003, bugfixes
  • 28/01/2005 -- version 1.002, bugfixes for 1.001
    • Note:
      before upgrading to this version read the notes for version 1.001
    • Fixed name resolution algorithm in gps and gps-db-update.pl, should stop getting invalid host strings for valid host names
      • The host name gets calculated by using removing everything up to and including the first dot
        • If the host name does not contain a dot the whole string is taken (I have noticed some mailhosts that resolve to 'localhost', seriously)
    • Fixed a problem with quoting ' in E-mail addresses when the old dbi library is used
    • Reenabled whitelisting network module
    • Fixed a bug in INSERT command, the order of sender, recipient was wrong
  • 25/01/2005 -- version 1.001, major changes
    • Changed the database structure. It is not compatible with the 0.x series.
    • Added new greylisting mode reverse: see description under mode
    • IP addresses are now stored numerically (see notes version 0.92)
    • weakbytes now functional but better use mode reverse and fallover to weak if necessary
    • Renamed the Triplets table to triplet (this should make it easier to track version change problems)
    • Added a PERL script for database upgrade gps-db-update.pl -- read comments in the file carefully before using
  • 13/01/2005 -- version 0.93, small bugfixes (last stable in 0.x series)
    • fixed a bug that prevented creating the whitelisting tables in mode init
      • fixed the problem with empty comments in whitelisting tables
  • 20/11/2004 -- version 0.92
    • changed the behaviour when something goes wrong during the init phase: gps used to exit and report the error. This resulted in REJECT for the message. Typically, this happened when the db is down. Now gps will continue and let all messages through without performing any checks. If this happens the logfile will contain gps entries saying NO GREYLISTING WILL BE DONE
    • Fixed the regex pattern support so it works with multiple conditions. E.g. now a pattern that expresses "sender=user@domain.org AND recipient=myuser@mydomain.org" can be used. For details see wl_pattern
    • Experimental Support for a new db scheme that stores client addresses as numerics instead of strings
      • This should speed up weak greylisting
      • Added weakbytes to the config options -- not functional yet
      • Note that this is disabled by default, comment out this line in defs.h to test it:
         #define OLDWEAK (true) 
        
        
        There is also a db conversion script gps-db-update.pl(in PERL) but note that the conversion procedure and table names might change
      • Added 4 new columns to the table. To upgrade an existing db use alter table before installing this version.
      • This is what the table should look like (the order of fields is significant):
        mysql> explain Triplets;
        +----------------+--------------+------+-----+---------+-------+
        | Field          | Type         | Null | Key | Default | Extra |
        +----------------+--------------+------+-----+---------+-------+
        | client_address | varchar(40)  |      | PRI |         |       |
        | recipient      | varchar(160) |      | PRI |         |       |
        | sender         | varchar(160) |      | PRI |         |       |
        | ip64           | decimal(4,0) |      | PRI | 0       |       |
        | ip32           | decimal(4,0) |      | PRI | 0       |       |
        | ip16           | decimal(4,0) |      | PRI | 0       |       |
        | ip8            | decimal(4,0) |      | PRI | 0       |       |
        | count          | int(11)      |      |     | 0       |       |
        | uts            | int(11)      |      |     | 0       |       |
        +----------------+--------------+------+-----+---------+-------+
        

  • 18/10/2004 -- version 0.91 pattern based whitelisting
    • added support for whitelisting based on regular expressions: See wl_pattern for details of this powerful feature.
  • 18/09/2004 -- version 0.9 bugfixes
    • fixed SQL incompatibilties with SQLite and postgresql
    • fixed some bugs with empty / non-existing whitelisting database tables
    • included example configuration files for SQLite and postgresql
    • fixed a typo in gps-maintain.pl thanks to a post in the gps Forum
  • 14/09/2004 -- version 0.8 whitelisting revisited
    • changed to a wl modules based system: wl_sender, wl_recipient
    • wl modules now offer dbcached mode: reads database at start and keeps it in memory instead of querying db
    • optimised indices for triplets table
  • 13/07/2004 -- version 0.7b small bugfixes
    • added configure option --with-syslog-facility={facility} default: LOG_MAIL
    • fixed bug in CREATE TABLE network
    • fixed detection and support for new libdbi dbi_conn_queryf (libdbi version >= 0.7.2)
  • 07/07/2004 -- version 0.7b
    • Added whitelisting support: wl_network
  • 06/07/2004 -- version 0.7
    • Changed support for weak greylisting. Use mode=weak
    • Changed sql statements to work with pgsql
    • Changed Triplets table index
  • 09/06/2004 -- version 0.6: Added support for weak greylisting: use ignorelastbyte=true in the config file (see included example) to allow any address within the same netblock

Download

Since version 1.005 gps source code is hosted on sourceforge. There are now two (three) ways of getting gps.

To checkout the current stable version use subversion (svn):

svn co https://greylist.svn.sourceforge.net/svnroot/greylist/trunk greylist
cd greylist
make -f Makefile.cvs
./configure
make
make install

The current development version is available from subversion on sourceforge

  • Using SVN:
     svn co https://greylist.svn.sourceforge.net/svnroot/greylist greylist
    

Older versions are not available fom this web site anymore. Older versions will be available from this site but mainly for archiving reasons. If you are using one of them consider upgrading to the most recent stable version.

Note:
Upgrading from 0.x versions to 1.x versions requires upgrading the database to the new database format. The distribution includes an upgrade script gps-db-update.pl It contains more information on how to perform the upgrade.

Older Releases

Discussion & Support

I have set up a publicly accessible forum where you can post bug reports, questions, and answers (preferably) around gps.

The gps Forum

Links

Let me know if you have a link to add here.