summaryrefslogtreecommitdiffstats
path: root/src/auth/auth-penalty.c
blob: 3816902a0a2c03cc714916575e51e1311069bebd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "ioloop.h"
#include "net.h"
#include "crc32.h"
#include "master-service.h"
#include "anvil-client.h"
#include "auth-request.h"
#include "auth-penalty.h"

#include <stdio.h>

/* We don't want IPv6 hosts being able to flood our penalty
   tracking with tons of different IPs. */
#define PENALTY_IPV6_MASK_BITS 48

struct auth_penalty_request {
	struct auth_request *auth_request;
	struct anvil_client *client;
	auth_penalty_callback_t *callback;
};

struct auth_penalty {
	struct anvil_client *client;

	bool disabled:1;
};

struct auth_penalty *auth_penalty_init(const char *path)
{
	struct auth_penalty *penalty;

	penalty = i_new(struct auth_penalty, 1);
	penalty->client = anvil_client_init(path, NULL,
					    ANVIL_CLIENT_FLAG_HIDE_ENOENT);
	if (anvil_client_connect(penalty->client, TRUE) < 0)
		penalty->disabled = TRUE;
	else {
		anvil_client_cmd(penalty->client, t_strdup_printf(
			"PENALTY-SET-EXPIRE-SECS\t%u", AUTH_PENALTY_TIMEOUT));
	}
	return penalty;
}

void auth_penalty_deinit(struct auth_penalty **_penalty)
{
	struct auth_penalty *penalty = *_penalty;

	*_penalty = NULL;
	anvil_client_deinit(&penalty->client);
	i_free(penalty);
}

unsigned int auth_penalty_to_secs(unsigned int penalty)
{
	unsigned int i, secs = AUTH_PENALTY_INIT_SECS;

	for (i = 0; i < penalty; i++)
		secs *= 2;
	return secs < AUTH_PENALTY_MAX_SECS ? secs : AUTH_PENALTY_MAX_SECS;
}

static void auth_penalty_anvil_callback(const char *reply, void *context)
{
	struct auth_penalty_request *request = context;
	unsigned int penalty = 0;
	unsigned long last_penalty = 0;
	unsigned int secs, drop_penalty;

	if (reply == NULL) {
		/* internal failure. */
		if (!anvil_client_is_connected(request->client)) {
			/* we probably didn't have permissions to reconnect
			   back to anvil. need to restart ourself. */
			master_service_stop(master_service);
		}
	} else if (sscanf(reply, "%u %lu", &penalty, &last_penalty) != 2) {
		e_error(request->auth_request->event,
			"Invalid PENALTY-GET reply: %s", reply);
	} else {
		if ((time_t)last_penalty > ioloop_time) {
			/* time moved backwards? */
			last_penalty = ioloop_time;
		}

		/* update penalty. */
		drop_penalty = AUTH_PENALTY_MAX_PENALTY;
		while (penalty > 0) {
			secs = auth_penalty_to_secs(drop_penalty);
			if (ioloop_time - last_penalty < secs)
				break;
			drop_penalty--;
			penalty--;
		}
	}

	request->callback(penalty, request->auth_request);
	auth_request_unref(&request->auth_request);
	i_free(request);
}

static const char *
auth_penalty_get_ident(struct auth_request *auth_request)
{
	struct ip_addr ip;

	ip = auth_request->fields.remote_ip;
	if (IPADDR_IS_V6(&ip)) {
		memset(ip.u.ip6.s6_addr + PENALTY_IPV6_MASK_BITS/CHAR_BIT, 0,
		       sizeof(ip.u.ip6.s6_addr) -
		       PENALTY_IPV6_MASK_BITS/CHAR_BIT);
	}
	return net_ip2addr(&ip);
}

void auth_penalty_lookup(struct auth_penalty *penalty,
			 struct auth_request *auth_request,
			 auth_penalty_callback_t *callback)
{
	struct auth_penalty_request *request;
	const char *ident;

	ident = auth_penalty_get_ident(auth_request);
	if (penalty->disabled || ident == NULL ||
	    auth_request->fields.no_penalty) {
		callback(0, auth_request);
		return;
	}

	request = i_new(struct auth_penalty_request, 1);
	request->auth_request = auth_request;
	request->client = penalty->client;
	request->callback = callback;
	auth_request_ref(auth_request);

	T_BEGIN {
		anvil_client_query(penalty->client,
				   t_strdup_printf("PENALTY-GET\t%s", ident),
				   auth_penalty_anvil_callback, request);
	} T_END;
}

static unsigned int
get_userpass_checksum(struct auth_request *auth_request)
{
	return auth_request->mech_password == NULL ? 0 :
		crc32_str_more(crc32_str(auth_request->mech_password),
			       auth_request->fields.user);
}

void auth_penalty_update(struct auth_penalty *penalty,
			 struct auth_request *auth_request, unsigned int value)
{
	const char *ident;

	ident = auth_penalty_get_ident(auth_request);
	if (penalty->disabled || ident == NULL ||
	    auth_request->fields.no_penalty)
		return;

	if (value > AUTH_PENALTY_MAX_PENALTY) {
		/* even if the actual value doesn't change, the last_change
		   timestamp does. */
		value = AUTH_PENALTY_MAX_PENALTY;
	}
	T_BEGIN {
		const char *cmd;
		unsigned int checksum;

		checksum = value == 0 ? 0 : get_userpass_checksum(auth_request);
		cmd = t_strdup_printf("PENALTY-INC\t%s\t%u\t%u",
				      ident, checksum, value);
		anvil_client_cmd(penalty->client, cmd);
	} T_END;
}