Browse Source

Add initial automated connection throttling

tags/v0.6.0
Daniel Oaks 7 years ago
parent
commit
91d59575ce
6 changed files with 301 additions and 53 deletions
  1. 4
    2
      CHANGELOG.md
  2. 36
    10
      irc/config.go
  3. 11
    0
      irc/connection_limits.go
  4. 142
    0
      irc/connection_throttling.go
  5. 77
    41
      irc/server.go
  6. 31
    0
      oragono.yaml

+ 4
- 2
CHANGELOG.md View File

@@ -11,15 +11,17 @@ New release of Oragono!
11 11
 
12 12
 ### Added
13 13
 * Added ARM build (for Raspberry PIs and similar).
14
-* Added `KLINE` and `UNDLINE` commands. Complementing `KLINE`, this lets you ban masks from the server.
14
+* Added automated connection throttling! To enable this, copy the `connection-throttling` section from the config.
15
+* Added `KLINE` and `UNDLINE` commands. Complementing `DLINE`'s per-IP and per-network bans, this lets you ban masks from the server.
15 16
 
16 17
 ### Changed
18
+* Connection limits can now be freely enabled or disabled. To enable automated limit handling, see the new `enabled` flag in the config, under `connection-limits`.
17 19
 
18 20
 ### Removed
19 21
 
20 22
 ### Fixed
21 23
 * Fixed an issue where `UNDLINE` didn't save across server launches.
22
-* Removed several race conditions and made the server more resiliant to these bugs.
24
+* Removed several race conditions which could result in server panics.
23 25
 
24 26
 
25 27
 ## [0.5.0] - 2016-12-10

+ 36
- 10
irc/config.go View File

@@ -12,6 +12,7 @@ import (
12 12
 	"io/ioutil"
13 13
 	"log"
14 14
 	"strings"
15
+	"time"
15 16
 
16 17
 	"gopkg.in/yaml.v2"
17 18
 )
@@ -95,12 +96,26 @@ type RestAPIConfig struct {
95 96
 }
96 97
 
97 98
 type ConnectionLimitsConfig struct {
99
+	Enabled     bool
98 100
 	CidrLenIPv4 int `yaml:"cidr-len-ipv4"`
99 101
 	CidrLenIPv6 int `yaml:"cidr-len-ipv6"`
100 102
 	IPsPerCidr  int `yaml:"ips-per-subnet"`
101 103
 	Exempted    []string
102 104
 }
103 105
 
106
+type ConnectionThrottleConfig struct {
107
+	Enabled            bool
108
+	CidrLenIPv4        int           `yaml:"cidr-len-ipv4"`
109
+	CidrLenIPv6        int           `yaml:"cidr-len-ipv6"`
110
+	ConnectionsPerCidr int           `yaml:"max-connections"`
111
+	DurationString     string        `yaml:"duration"`
112
+	Duration           time.Duration `yaml:"duration-time"`
113
+	BanDurationString  string        `yaml:"ban-duration"`
114
+	BanDuration        time.Duration
115
+	BanMessage         string `yaml:"ban-message"`
116
+	Exempted           []string
117
+}
118
+
104 119
 type Config struct {
105 120
 	Network struct {
106 121
 		Name string
@@ -108,16 +123,17 @@ type Config struct {
108 123
 
109 124
 	Server struct {
110 125
 		PassConfig
111
-		Password         string
112
-		Name             string
113
-		Listen           []string
114
-		Wslisten         string                      `yaml:"ws-listen"`
115
-		TLSListeners     map[string]*TLSListenConfig `yaml:"tls-listeners"`
116
-		RestAPI          RestAPIConfig               `yaml:"rest-api"`
117
-		CheckIdent       bool                        `yaml:"check-ident"`
118
-		Log              string
119
-		MOTD             string
120
-		ConnectionLimits ConnectionLimitsConfig `yaml:"connection-limits"`
126
+		Password           string
127
+		Name               string
128
+		Listen             []string
129
+		Wslisten           string                      `yaml:"ws-listen"`
130
+		TLSListeners       map[string]*TLSListenConfig `yaml:"tls-listeners"`
131
+		RestAPI            RestAPIConfig               `yaml:"rest-api"`
132
+		CheckIdent         bool                        `yaml:"check-ident"`
133
+		Log                string
134
+		MOTD               string
135
+		ConnectionLimits   ConnectionLimitsConfig   `yaml:"connection-limits"`
136
+		ConnectionThrottle ConnectionThrottleConfig `yaml:"connection-throttling"`
121 137
 	}
122 138
 
123 139
 	Datastore struct {
@@ -309,6 +325,16 @@ func LoadConfig(filename string) (config *Config, err error) {
309 325
 	if config.Limits.NickLen < 1 || config.Limits.ChannelLen < 2 || config.Limits.AwayLen < 1 || config.Limits.KickLen < 1 || config.Limits.TopicLen < 1 {
310 326
 		return nil, errors.New("Limits aren't setup properly, check them and make them sane")
311 327
 	}
328
+	if config.Server.ConnectionThrottle.Enabled {
329
+		config.Server.ConnectionThrottle.Duration, err = time.ParseDuration(config.Server.ConnectionThrottle.DurationString)
330
+		if err != nil {
331
+			return nil, fmt.Errorf("Could not parse connection-throttle duration: %s", err.Error())
332
+		}
333
+		config.Server.ConnectionThrottle.BanDuration, err = time.ParseDuration(config.Server.ConnectionThrottle.BanDurationString)
334
+		if err != nil {
335
+			return nil, fmt.Errorf("Could not parse connection-throttle ban-duration: %s", err.Error())
336
+		}
337
+	}
312 338
 
313 339
 	return config, nil
314 340
 }

+ 11
- 0
irc/connection_limits.go View File

@@ -15,6 +15,7 @@ var (
15 15
 
16 16
 // ConnectionLimits manages the automated client connection limits.
17 17
 type ConnectionLimits struct {
18
+	enabled  bool
18 19
 	ipv4Mask net.IPMask
19 20
 	ipv6Mask net.IPMask
20 21
 	// subnetLimit is the maximum number of clients per subnet
@@ -44,6 +45,10 @@ func (cl *ConnectionLimits) maskAddr(addr net.IP) net.IP {
44 45
 // AddClient adds a client to our population if possible. If we can't, throws an error instead.
45 46
 // 'force' is used to add already-existing clients (i.e. ones that are already on the network).
46 47
 func (cl *ConnectionLimits) AddClient(addr net.IP, force bool) error {
48
+	if !cl.enabled {
49
+		return nil
50
+	}
51
+
47 52
 	// check exempted lists
48 53
 	// we don't track populations for exempted addresses or nets - this is by design
49 54
 	if cl.exemptedIPs[addr.String()] {
@@ -70,6 +75,10 @@ func (cl *ConnectionLimits) AddClient(addr net.IP, force bool) error {
70 75
 
71 76
 // RemoveClient removes the given address from our population
72 77
 func (cl *ConnectionLimits) RemoveClient(addr net.IP) {
78
+	if !cl.enabled {
79
+		return
80
+	}
81
+
73 82
 	addrString := addr.String()
74 83
 	cl.population[addrString] = cl.population[addrString] - 1
75 84
 
@@ -82,6 +91,8 @@ func (cl *ConnectionLimits) RemoveClient(addr net.IP) {
82 91
 // NewConnectionLimits returns a new connection limit handler.
83 92
 func NewConnectionLimits(config ConnectionLimitsConfig) (*ConnectionLimits, error) {
84 93
 	var cl ConnectionLimits
94
+	cl.enabled = config.Enabled
95
+
85 96
 	cl.population = make(map[string]int)
86 97
 	cl.exemptedIPs = make(map[string]bool)
87 98
 

+ 142
- 0
irc/connection_throttling.go View File

@@ -0,0 +1,142 @@
1
+// Copyright (c) 2016- Daniel Oaks <daniel@danieloaks.net>
2
+// released under the MIT license
3
+
4
+package irc
5
+
6
+import (
7
+	"fmt"
8
+	"net"
9
+	"time"
10
+
11
+	"github.com/DanielOaks/girc-go/ircmsg"
12
+)
13
+
14
+// ThrottleDetails holds the connection-throttling details for a subnet/IP.
15
+type ThrottleDetails struct {
16
+	Start       time.Time
17
+	ClientCount int
18
+}
19
+
20
+// ConnectionThrottle manages automated client connection throttling.
21
+type ConnectionThrottle struct {
22
+	enabled     bool
23
+	ipv4Mask    net.IPMask
24
+	ipv6Mask    net.IPMask
25
+	subnetLimit int
26
+	duration    time.Duration
27
+	population  map[string]ThrottleDetails
28
+
29
+	// used by the server to ban clients that go over this limit
30
+	BanDuration     time.Duration
31
+	BanMessage      string
32
+	BanMessageBytes []byte
33
+
34
+	// exemptedIPs holds IPs that are exempt from limits
35
+	exemptedIPs map[string]bool
36
+	// exemptedNets holds networks that are exempt from limits
37
+	exemptedNets []net.IPNet
38
+}
39
+
40
+// maskAddr masks the given IPv4/6 address with our cidr limit masks.
41
+func (ct *ConnectionThrottle) maskAddr(addr net.IP) net.IP {
42
+	if addr.To4() == nil {
43
+		// IPv6 addr
44
+		addr = addr.Mask(ct.ipv6Mask)
45
+	} else {
46
+		// IPv4 addr
47
+		addr = addr.Mask(ct.ipv4Mask)
48
+	}
49
+
50
+	return addr
51
+}
52
+
53
+// ResetFor removes any existing count for the given address.
54
+func (ct *ConnectionThrottle) ResetFor(addr net.IP) {
55
+	if !ct.enabled {
56
+		return
57
+	}
58
+
59
+	// remove
60
+	ct.maskAddr(addr)
61
+	addrString := addr.String()
62
+	delete(ct.population, addrString)
63
+}
64
+
65
+// AddClient introduces a new client connection if possible. If we can't, throws an error instead.
66
+func (ct *ConnectionThrottle) AddClient(addr net.IP) error {
67
+	if !ct.enabled {
68
+		return nil
69
+	}
70
+
71
+	// check exempted lists
72
+	if ct.exemptedIPs[addr.String()] {
73
+		return nil
74
+	}
75
+	for _, ex := range ct.exemptedNets {
76
+		if ex.Contains(addr) {
77
+			return nil
78
+		}
79
+	}
80
+
81
+	// check throttle
82
+	ct.maskAddr(addr)
83
+	addrString := addr.String()
84
+
85
+	details, exists := ct.population[addrString]
86
+	if !exists || details.Start.Add(ct.duration).Before(time.Now()) {
87
+		details = ThrottleDetails{
88
+			Start: time.Now(),
89
+		}
90
+	}
91
+
92
+	if details.ClientCount+1 > ct.subnetLimit {
93
+		return errTooManyClients
94
+	}
95
+
96
+	details.ClientCount++
97
+	ct.population[addrString] = details
98
+
99
+	return nil
100
+}
101
+
102
+// NewConnectionThrottle returns a new client connection throttler.
103
+func NewConnectionThrottle(config ConnectionThrottleConfig) (*ConnectionThrottle, error) {
104
+	var ct ConnectionThrottle
105
+	ct.enabled = config.Enabled
106
+
107
+	ct.population = make(map[string]ThrottleDetails)
108
+	ct.exemptedIPs = make(map[string]bool)
109
+
110
+	ct.ipv4Mask = net.CIDRMask(config.CidrLenIPv4, 32)
111
+	ct.ipv6Mask = net.CIDRMask(config.CidrLenIPv6, 128)
112
+	ct.subnetLimit = config.ConnectionsPerCidr
113
+
114
+	ct.duration = config.Duration
115
+
116
+	ct.BanDuration = config.BanDuration
117
+	ct.BanMessage = config.BanMessage
118
+	ircmsgOutput := ircmsg.MakeMessage(nil, "", "ERROR", ct.BanMessage)
119
+	msg, err := ircmsgOutput.Line()
120
+	if err != nil {
121
+		return nil, fmt.Errorf("Could not make error message: %s", err.Error())
122
+	}
123
+	ct.BanMessageBytes = []byte(msg)
124
+
125
+	// assemble exempted nets
126
+	for _, cidr := range config.Exempted {
127
+		ipaddr := net.ParseIP(cidr)
128
+		_, netaddr, err := net.ParseCIDR(cidr)
129
+
130
+		if ipaddr == nil && err != nil {
131
+			return nil, fmt.Errorf("Could not parse exempted IP/network [%s]", cidr)
132
+		}
133
+
134
+		if ipaddr != nil {
135
+			ct.exemptedIPs[ipaddr.String()] = true
136
+		} else {
137
+			ct.exemptedNets = append(ct.exemptedNets, *netaddr)
138
+		}
139
+	}
140
+
141
+	return &ct, nil
142
+}

+ 77
- 41
irc/server.go View File

@@ -28,7 +28,7 @@ import (
28 28
 
29 29
 var (
30 30
 	// cached because this may be used lots
31
-	tooManyClientsMsg      = ircmsg.MakeMessage(nil, "", "ERROR", "Too many clients from your IP or network")
31
+	tooManyClientsMsg      = ircmsg.MakeMessage(nil, "", "ERROR", "Too many clients from your network")
32 32
 	tooManyClientsBytes, _ = tooManyClientsMsg.Line()
33 33
 
34 34
 	bannedFromServerMsg      = ircmsg.MakeMessage(nil, "", "ERROR", "You are banned from this server (%s)")
@@ -72,42 +72,44 @@ type ListenerEvent struct {
72 72
 
73 73
 // Server is the main Oragono server.
74 74
 type Server struct {
75
-	accountRegistration   *AccountRegistration
76
-	accounts              map[string]*ClientAccount
77
-	authenticationEnabled bool
78
-	channels              ChannelNameMap
79
-	checkIdent            bool
80
-	clients               *ClientLookupSet
81
-	commands              chan Command
82
-	configFilename        string
83
-	connectionLimits      *ConnectionLimits
84
-	connectionLimitsMutex sync.Mutex // used when affecting the connection limiter, to make sure rehashing doesn't make things go out-of-whack
85
-	ctime                 time.Time
86
-	currentOpers          map[*Client]bool
87
-	dlines                *DLineManager
88
-	idle                  chan *Client
89
-	isupport              *ISupportList
90
-	klines                *KLineManager
91
-	limits                Limits
92
-	listenerEventActMutex sync.Mutex
93
-	listeners             map[string]ListenerInterface
94
-	listenerUpdateMutex   sync.Mutex
95
-	monitoring            map[string][]Client
96
-	motdLines             []string
97
-	name                  string
98
-	nameCasefolded        string
99
-	networkName           string
100
-	newConns              chan clientConn
101
-	operators             map[string]Oper
102
-	operclasses           map[string]OperClass
103
-	password              []byte
104
-	passwords             *PasswordManager
105
-	rehashMutex           sync.Mutex
106
-	rehashSignal          chan os.Signal
107
-	restAPI               *RestAPIConfig
108
-	signals               chan os.Signal
109
-	store                 *buntdb.DB
110
-	whoWas                *WhoWasList
75
+	accountRegistration     *AccountRegistration
76
+	accounts                map[string]*ClientAccount
77
+	authenticationEnabled   bool
78
+	channels                ChannelNameMap
79
+	checkIdent              bool
80
+	clients                 *ClientLookupSet
81
+	commands                chan Command
82
+	configFilename          string
83
+	connectionThrottle      *ConnectionThrottle
84
+	connectionThrottleMutex sync.Mutex // used when affecting the connection limiter, to make sure rehashing doesn't make things go out-of-whack
85
+	connectionLimits        *ConnectionLimits
86
+	connectionLimitsMutex   sync.Mutex // used when affecting the connection limiter, to make sure rehashing doesn't make things go out-of-whack
87
+	ctime                   time.Time
88
+	currentOpers            map[*Client]bool
89
+	dlines                  *DLineManager
90
+	idle                    chan *Client
91
+	isupport                *ISupportList
92
+	klines                  *KLineManager
93
+	limits                  Limits
94
+	listenerEventActMutex   sync.Mutex
95
+	listeners               map[string]ListenerInterface
96
+	listenerUpdateMutex     sync.Mutex
97
+	monitoring              map[string][]Client
98
+	motdLines               []string
99
+	name                    string
100
+	nameCasefolded          string
101
+	networkName             string
102
+	newConns                chan clientConn
103
+	operators               map[string]Oper
104
+	operclasses             map[string]OperClass
105
+	password                []byte
106
+	passwords               *PasswordManager
107
+	rehashMutex             sync.Mutex
108
+	rehashSignal            chan os.Signal
109
+	restAPI                 *RestAPIConfig
110
+	signals                 chan os.Signal
111
+	store                   *buntdb.DB
112
+	whoWas                  *WhoWasList
111 113
 }
112 114
 
113 115
 var (
@@ -157,6 +159,10 @@ func NewServer(configFilename string, config *Config) *Server {
157 159
 	if err != nil {
158 160
 		log.Fatal("Error loading connection limits:", err.Error())
159 161
 	}
162
+	connectionThrottle, err := NewConnectionThrottle(config.Server.ConnectionThrottle)
163
+	if err != nil {
164
+		log.Fatal("Error loading connection throttler:", err.Error())
165
+	}
160 166
 
161 167
 	server := &Server{
162 168
 		accounts:              make(map[string]*ClientAccount),
@@ -166,6 +172,7 @@ func NewServer(configFilename string, config *Config) *Server {
166 172
 		commands:              make(chan Command),
167 173
 		configFilename:        configFilename,
168 174
 		connectionLimits:      connectionLimits,
175
+		connectionThrottle:    connectionThrottle,
169 176
 		ctime:                 time.Now(),
170 177
 		currentOpers:          make(map[*Client]bool),
171 178
 		idle:                  make(chan *Client),
@@ -403,6 +410,27 @@ func (server *Server) Run() {
403 410
 					continue
404 411
 				}
405 412
 
413
+				// check connection throttle
414
+				server.connectionThrottleMutex.Lock()
415
+				err = server.connectionThrottle.AddClient(ipaddr)
416
+				server.connectionThrottleMutex.Unlock()
417
+				if err != nil {
418
+					// too many connections too quickly from client, tell them and close the connection
419
+					length := &IPRestrictTime{
420
+						Duration: server.connectionThrottle.BanDuration,
421
+						Expires:  time.Now().Add(server.connectionThrottle.BanDuration),
422
+					}
423
+					server.dlines.AddIP(ipaddr, length, server.connectionThrottle.BanMessage, "Exceeded automated connection throttle")
424
+
425
+					// reset ban on connectionThrottle
426
+					server.connectionThrottle.ResetFor(ipaddr)
427
+
428
+					// this might not show up properly on some clients, but our objective here is just to close it out before it has a load impact on us
429
+					conn.Conn.Write([]byte(server.connectionThrottle.BanMessageBytes))
430
+					conn.Conn.Close()
431
+					continue
432
+				}
433
+
406 434
 				go NewClient(server, conn.Conn, conn.IsTLS)
407 435
 				continue
408 436
 			}
@@ -1066,23 +1094,29 @@ func (server *Server) rehash() error {
1066 1094
 	config, err := LoadConfig(server.configFilename)
1067 1095
 
1068 1096
 	if err != nil {
1069
-		return fmt.Errorf("Error rehashing config file: %s", err.Error())
1097
+		return fmt.Errorf("Error rehashing config file config: %s", err.Error())
1070 1098
 	}
1071 1099
 
1072 1100
 	// confirm connectionLimits are fine
1073 1101
 	connectionLimits, err := NewConnectionLimits(config.Server.ConnectionLimits)
1074 1102
 	if err != nil {
1075
-		return fmt.Errorf("Error rehashing config file: %s", err.Error())
1103
+		return fmt.Errorf("Error rehashing config file connection-limits: %s", err.Error())
1104
+	}
1105
+
1106
+	// confirm connectionThrottler is fine
1107
+	connectionThrottle, err := NewConnectionThrottle(config.Server.ConnectionThrottle)
1108
+	if err != nil {
1109
+		return fmt.Errorf("Error rehashing config file connection-throttle: %s", err.Error())
1076 1110
 	}
1077 1111
 
1078 1112
 	// confirm operator stuff all exists and is fine
1079 1113
 	operclasses, err := config.OperatorClasses()
1080 1114
 	if err != nil {
1081
-		return fmt.Errorf("Error rehashing config file: %s", err.Error())
1115
+		return fmt.Errorf("Error rehashing config file operclasses: %s", err.Error())
1082 1116
 	}
1083 1117
 	opers, err := config.Operators(operclasses)
1084 1118
 	if err != nil {
1085
-		return fmt.Errorf("Error rehashing config file: %s", err.Error())
1119
+		return fmt.Errorf("Error rehashing config file opers: %s", err.Error())
1086 1120
 	}
1087 1121
 	for client := range server.currentOpers {
1088 1122
 		_, exists := opers[client.operName]
@@ -1094,6 +1128,8 @@ func (server *Server) rehash() error {
1094 1128
 	// apply new connectionlimits
1095 1129
 	server.connectionLimitsMutex.Lock()
1096 1130
 	server.connectionLimits = connectionLimits
1131
+	server.connectionThrottleMutex.Lock()
1132
+	server.connectionThrottle = connectionThrottle
1097 1133
 
1098 1134
 	server.clients.ByNickMutex.RLock()
1099 1135
 	for _, client := range server.clients.ByNick {

+ 31
- 0
oragono.yaml View File

@@ -51,6 +51,9 @@ server:
51 51
 
52 52
     # maximum number of connections per subnet
53 53
     connection-limits:
54
+        # whether to throttle limits or not
55
+        enabled: true
56
+
54 57
         # how wide the cidr should be for IPv4 
55 58
         cidr-len-ipv4: 24
56 59
 
@@ -66,6 +69,34 @@ server:
66 69
             - "127.0.0.1/8"
67 70
             - "::1/128"
68 71
 
72
+    # automated connection throttling
73
+    connection-throttling:
74
+        # whether to throttle connections or not
75
+        enabled: true
76
+
77
+        # how wide the cidr should be for IPv4 
78
+        cidr-len-ipv4: 32
79
+
80
+        # how wide the cidr should be for IPv6
81
+        cidr-len-ipv6: 128
82
+
83
+        # how long to keep track of connections for
84
+        duration: 10m
85
+
86
+        # maximum number of connections, per subnet, within the given duration
87
+        max-connections: 12
88
+
89
+        # how long to ban offenders for, and the message to use
90
+        # after banning them, the number of connections is reset (which lets you use UNDLINE to unban people)
91
+        ban-duration: 10m
92
+        ban-message: You have attempted to connect too many times within a short duration. Wait a while, and you will be able to connect.
93
+
94
+        # IPs/networks which are exempted from connection limits
95
+        exempted:
96
+            - "127.0.0.1"
97
+            - "127.0.0.1/8"
98
+            - "::1/128"
99
+
69 100
 # account/channel registration
70 101
 registration:
71 102
     # account registration

Loading…
Cancel
Save