Browse Source

Merge pull request #1445 from slingamn/flatiptypes.5

introduce "flat ip" representations
tags/v2.5.0-rc1
Shivaram Lingamneni 3 years ago
parent
commit
58edabf5c3
No account linked to committer's email address

+ 1
- 0
Makefile View File

@@ -25,6 +25,7 @@ test:
25 25
 	cd irc/cloaks && go test . && go vet .
26 26
 	cd irc/connection_limits && go test . && go vet .
27 27
 	cd irc/email && go test . && go vet .
28
+	cd irc/flatip && go test . && go vet .
28 29
 	cd irc/history && go test . && go vet .
29 30
 	cd irc/isupport && go test . && go vet .
30 31
 	cd irc/migrations && go test . && go vet .

+ 0
- 3
default.yaml View File

@@ -247,9 +247,6 @@ server:
247 247
         window: 10m
248 248
         # maximum number of new connections per IP/CIDR within the given duration
249 249
         max-connections-per-window: 32
250
-        # how long to ban offenders for. after banning them, the number of connections is
251
-        # reset, which lets you use /UNDLINE to unban people
252
-        throttle-ban-duration: 10m
253 250
 
254 251
         # how wide the CIDR should be for IPv4 (a /32 is a fully specified IPv4 address)
255 252
         cidr-len-ipv4: 32

+ 2
- 1
irc/client.go View File

@@ -21,6 +21,7 @@ import (
21 21
 	ident "github.com/oragono/go-ident"
22 22
 	"github.com/oragono/oragono/irc/caps"
23 23
 	"github.com/oragono/oragono/irc/connection_limits"
24
+	"github.com/oragono/oragono/irc/flatip"
24 25
 	"github.com/oragono/oragono/irc/history"
25 26
 	"github.com/oragono/oragono/irc/modes"
26 27
 	"github.com/oragono/oragono/irc/sno"
@@ -1477,7 +1478,7 @@ func (client *Client) destroy(session *Session) {
1477 1478
 			if session.proxiedIP != nil {
1478 1479
 				ip = session.proxiedIP
1479 1480
 			}
1480
-			client.server.connectionLimiter.RemoveClient(ip)
1481
+			client.server.connectionLimiter.RemoveClient(flatip.FromNetIP(ip))
1481 1482
 			source = ip.String()
1482 1483
 		}
1483 1484
 		client.server.logger.Info("connect-ip", fmt.Sprintf("disconnecting session of %s from %s", details.nick, source))

+ 53
- 49
irc/connection_limits/limiter.go View File

@@ -4,12 +4,13 @@
4 4
 package connection_limits
5 5
 
6 6
 import (
7
+	"crypto/md5"
7 8
 	"errors"
8 9
 	"fmt"
9
-	"net"
10 10
 	"sync"
11 11
 	"time"
12 12
 
13
+	"github.com/oragono/oragono/irc/flatip"
13 14
 	"github.com/oragono/oragono/irc/utils"
14 15
 )
15 16
 
@@ -26,10 +27,15 @@ type CustomLimitConfig struct {
26 27
 
27 28
 // tuples the key-value pair of a CIDR and its custom limit/throttle values
28 29
 type customLimit struct {
29
-	name          string
30
+	name          [16]byte
30 31
 	maxConcurrent int
31 32
 	maxPerWindow  int
32
-	nets          []net.IPNet
33
+	nets          []flatip.IPNet
34
+}
35
+
36
+type limiterKey struct {
37
+	maskedIP  flatip.IP
38
+	prefixLen uint8 // 0 for the fake nets we generate for custom limits
33 39
 }
34 40
 
35 41
 // LimiterConfig controls the automated connection limits.
@@ -41,8 +47,7 @@ type rawLimiterConfig struct {
41 47
 
42 48
 	Throttle     bool
43 49
 	Window       time.Duration
44
-	MaxPerWindow int           `yaml:"max-connections-per-window"`
45
-	BanDuration  time.Duration `yaml:"throttle-ban-duration"`
50
+	MaxPerWindow int `yaml:"max-connections-per-window"`
46 51
 
47 52
 	CidrLenIPv4 int `yaml:"cidr-len-ipv4"`
48 53
 	CidrLenIPv6 int `yaml:"cidr-len-ipv6"`
@@ -55,9 +60,7 @@ type rawLimiterConfig struct {
55 60
 type LimiterConfig struct {
56 61
 	rawLimiterConfig
57 62
 
58
-	ipv4Mask     net.IPMask
59
-	ipv6Mask     net.IPMask
60
-	exemptedNets []net.IPNet
63
+	exemptedNets []flatip.IPNet
61 64
 	customLimits []customLimit
62 65
 }
63 66
 
@@ -69,15 +72,19 @@ func (config *LimiterConfig) UnmarshalYAML(unmarshal func(interface{}) error) (e
69 72
 }
70 73
 
71 74
 func (config *LimiterConfig) postprocess() (err error) {
72
-	config.exemptedNets, err = utils.ParseNetList(config.Exempted)
75
+	exemptedNets, err := utils.ParseNetList(config.Exempted)
73 76
 	if err != nil {
74 77
 		return fmt.Errorf("Could not parse limiter exemption list: %v", err.Error())
75 78
 	}
79
+	config.exemptedNets = make([]flatip.IPNet, len(exemptedNets))
80
+	for i, exempted := range exemptedNets {
81
+		config.exemptedNets[i] = flatip.FromNetIPNet(exempted)
82
+	}
76 83
 
77 84
 	for identifier, customLimitConf := range config.CustomLimits {
78
-		nets := make([]net.IPNet, len(customLimitConf.Nets))
85
+		nets := make([]flatip.IPNet, len(customLimitConf.Nets))
79 86
 		for i, netStr := range customLimitConf.Nets {
80
-			normalizedNet, err := utils.NormalizedNetFromString(netStr)
87
+			normalizedNet, err := flatip.ParseToNormalizedNet(netStr)
81 88
 			if err != nil {
82 89
 				return fmt.Errorf("Bad net %s in custom-limits block %s: %w", netStr, identifier, err)
83 90
 			}
@@ -86,23 +93,20 @@ func (config *LimiterConfig) postprocess() (err error) {
86 93
 		if len(customLimitConf.Nets) == 0 {
87 94
 			// see #1421: this is the legacy config format where the
88 95
 			// dictionary key of the block is a CIDR string
89
-			normalizedNet, err := utils.NormalizedNetFromString(identifier)
96
+			normalizedNet, err := flatip.ParseToNormalizedNet(identifier)
90 97
 			if err != nil {
91 98
 				return fmt.Errorf("Custom limit block %s has no defined nets", identifier)
92 99
 			}
93
-			nets = []net.IPNet{normalizedNet}
100
+			nets = []flatip.IPNet{normalizedNet}
94 101
 		}
95 102
 		config.customLimits = append(config.customLimits, customLimit{
96 103
 			maxConcurrent: customLimitConf.MaxConcurrent,
97 104
 			maxPerWindow:  customLimitConf.MaxPerWindow,
98
-			name:          "*" + identifier,
105
+			name:          md5.Sum([]byte(identifier)),
99 106
 			nets:          nets,
100 107
 		})
101 108
 	}
102 109
 
103
-	config.ipv4Mask = net.CIDRMask(config.CidrLenIPv4, 32)
104
-	config.ipv6Mask = net.CIDRMask(config.CidrLenIPv6, 128)
105
-
106 110
 	return nil
107 111
 }
108 112
 
@@ -113,53 +117,56 @@ type Limiter struct {
113 117
 	config *LimiterConfig
114 118
 
115 119
 	// IP/CIDR -> count of clients connected from there:
116
-	limiter map[string]int
120
+	limiter map[limiterKey]int
117 121
 	// IP/CIDR -> throttle state:
118
-	throttler map[string]ThrottleDetails
122
+	throttler map[limiterKey]ThrottleDetails
119 123
 }
120 124
 
121 125
 // addrToKey canonicalizes `addr` to a string key, and returns
122 126
 // the relevant connection limit and throttle max-per-window values
123
-func (cl *Limiter) addrToKey(addr net.IP) (key string, limit int, throttle int) {
124
-	// `key` will be a CIDR string like "8.8.8.8/32" or "2001:0db8::/32"
127
+func (cl *Limiter) addrToKey(addr flatip.IP) (key limiterKey, limit int, throttle int) {
125 128
 	for _, custom := range cl.config.customLimits {
126 129
 		for _, net := range custom.nets {
127 130
 			if net.Contains(addr) {
128
-				return custom.name, custom.maxConcurrent, custom.maxPerWindow
131
+				return limiterKey{maskedIP: custom.name, prefixLen: 0}, custom.maxConcurrent, custom.maxPerWindow
129 132
 			}
130 133
 		}
131 134
 	}
132 135
 
133
-	var ipNet net.IPNet
134
-	addrv4 := addr.To4()
135
-	if addrv4 != nil {
136
-		ipNet = net.IPNet{
137
-			IP:   addrv4.Mask(cl.config.ipv4Mask),
138
-			Mask: cl.config.ipv4Mask,
139
-		}
136
+	var prefixLen int
137
+	if addr.IsIPv4() {
138
+		prefixLen = cl.config.CidrLenIPv4
139
+		addr = addr.Mask(prefixLen, 32)
140
+		prefixLen += 96
140 141
 	} else {
141
-		ipNet = net.IPNet{
142
-			IP:   addr.Mask(cl.config.ipv6Mask),
143
-			Mask: cl.config.ipv6Mask,
144
-		}
142
+		prefixLen = cl.config.CidrLenIPv6
143
+		addr = addr.Mask(prefixLen, 128)
145 144
 	}
146
-	return ipNet.String(), cl.config.MaxConcurrent, cl.config.MaxPerWindow
145
+
146
+	return limiterKey{maskedIP: addr, prefixLen: uint8(prefixLen)}, cl.config.MaxConcurrent, cl.config.MaxPerWindow
147 147
 }
148 148
 
149 149
 // AddClient adds a client to our population if possible. If we can't, throws an error instead.
150
-func (cl *Limiter) AddClient(addr net.IP) error {
150
+func (cl *Limiter) AddClient(addr flatip.IP) error {
151 151
 	cl.Lock()
152 152
 	defer cl.Unlock()
153 153
 
154 154
 	// we don't track populations for exempted addresses or nets - this is by design
155
-	if utils.IPInNets(addr, cl.config.exemptedNets) {
155
+	if flatip.IPInNets(addr, cl.config.exemptedNets) {
156 156
 		return nil
157 157
 	}
158 158
 
159 159
 	addrString, maxConcurrent, maxPerWindow := cl.addrToKey(addr)
160 160
 
161
-	// XXX check throttle first; if we checked limit first and then checked throttle,
162
-	// we'd have to decrement the limit on an unsuccessful throttle check
161
+	// check limiter
162
+	var count int
163
+	if cl.config.Count {
164
+		count = cl.limiter[addrString] + 1
165
+		if count > maxConcurrent {
166
+			return ErrLimitExceeded
167
+		}
168
+	}
169
+
163 170
 	if cl.config.Throttle {
164 171
 		details := cl.throttler[addrString] // retrieve mutable throttle state from the map
165 172
 		// add in constant state to process the limiting operation
@@ -171,16 +178,13 @@ func (cl *Limiter) AddClient(addr net.IP) error {
171 178
 		throttled, _ := g.Touch()                    // actually check the limit
172 179
 		cl.throttler[addrString] = g.ThrottleDetails // store modified mutable state
173 180
 		if throttled {
181
+			// back out the limiter add
174 182
 			return ErrThrottleExceeded
175 183
 		}
176 184
 	}
177 185
 
178
-	// now check limiter
186
+	// success, record in limiter
179 187
 	if cl.config.Count {
180
-		count := cl.limiter[addrString] + 1
181
-		if count > maxConcurrent {
182
-			return ErrLimitExceeded
183
-		}
184 188
 		cl.limiter[addrString] = count
185 189
 	}
186 190
 
@@ -188,11 +192,11 @@ func (cl *Limiter) AddClient(addr net.IP) error {
188 192
 }
189 193
 
190 194
 // RemoveClient removes the given address from our population
191
-func (cl *Limiter) RemoveClient(addr net.IP) {
195
+func (cl *Limiter) RemoveClient(addr flatip.IP) {
192 196
 	cl.Lock()
193 197
 	defer cl.Unlock()
194 198
 
195
-	if !cl.config.Count || utils.IPInNets(addr, cl.config.exemptedNets) {
199
+	if !cl.config.Count || flatip.IPInNets(addr, cl.config.exemptedNets) {
196 200
 		return
197 201
 	}
198 202
 
@@ -206,11 +210,11 @@ func (cl *Limiter) RemoveClient(addr net.IP) {
206 210
 }
207 211
 
208 212
 // ResetThrottle resets the throttle count for an IP
209
-func (cl *Limiter) ResetThrottle(addr net.IP) {
213
+func (cl *Limiter) ResetThrottle(addr flatip.IP) {
210 214
 	cl.Lock()
211 215
 	defer cl.Unlock()
212 216
 
213
-	if !cl.config.Throttle || utils.IPInNets(addr, cl.config.exemptedNets) {
217
+	if !cl.config.Throttle || flatip.IPInNets(addr, cl.config.exemptedNets) {
214 218
 		return
215 219
 	}
216 220
 
@@ -224,10 +228,10 @@ func (cl *Limiter) ApplyConfig(config *LimiterConfig) {
224 228
 	defer cl.Unlock()
225 229
 
226 230
 	if cl.limiter == nil {
227
-		cl.limiter = make(map[string]int)
231
+		cl.limiter = make(map[limiterKey]int)
228 232
 	}
229 233
 	if cl.throttler == nil {
230
-		cl.throttler = make(map[string]ThrottleDetails)
234
+		cl.throttler = make(map[limiterKey]ThrottleDetails)
231 235
 	}
232 236
 
233 237
 	cl.config = config

+ 16
- 9
irc/connection_limits/limiter_test.go View File

@@ -4,15 +4,17 @@
4 4
 package connection_limits
5 5
 
6 6
 import (
7
-	"net"
7
+	"crypto/md5"
8 8
 	"testing"
9 9
 	"time"
10
+
11
+	"github.com/oragono/oragono/irc/flatip"
10 12
 )
11 13
 
12
-func easyParseIP(ipstr string) (result net.IP) {
13
-	result = net.ParseIP(ipstr)
14
-	if result == nil {
15
-		panic(ipstr)
14
+func easyParseIP(ipstr string) (result flatip.IP) {
15
+	result, err := flatip.ParseIP(ipstr)
16
+	if err != nil {
17
+		panic(err)
16 18
 	}
17 19
 	return
18 20
 }
@@ -47,18 +49,23 @@ func TestKeying(t *testing.T) {
47 49
 	var limiter Limiter
48 50
 	limiter.ApplyConfig(&config)
49 51
 
52
+	// an ipv4 /32 looks like a /128 to us after applying the 4-in-6 mapping
50 53
 	key, maxConc, maxWin := limiter.addrToKey(easyParseIP("1.1.1.1"))
51
-	assertEqual(key, "1.1.1.1/32", t)
54
+	assertEqual(key.prefixLen, uint8(128), t)
55
+	assertEqual(key.maskedIP[12:], []byte{1, 1, 1, 1}, t)
52 56
 	assertEqual(maxConc, 4, t)
53 57
 	assertEqual(maxWin, 8, t)
54 58
 
55
-	key, maxConc, maxWin = limiter.addrToKey(easyParseIP("2607:5301:201:3100::7426"))
56
-	assertEqual(key, "2607:5301:201:3100::/64", t)
59
+	testIPv6 := easyParseIP("2607:5301:201:3100::7426")
60
+	key, maxConc, maxWin = limiter.addrToKey(testIPv6)
61
+	assertEqual(key.prefixLen, uint8(64), t)
62
+	assertEqual(flatip.IP(key.maskedIP), easyParseIP("2607:5301:201:3100::"), t)
57 63
 	assertEqual(maxConc, 4, t)
58 64
 	assertEqual(maxWin, 8, t)
59 65
 
60 66
 	key, maxConc, maxWin = limiter.addrToKey(easyParseIP("8.8.4.4"))
61
-	assertEqual(key, "*google", t)
67
+	assertEqual(key.prefixLen, uint8(0), t)
68
+	assertEqual([16]byte(key.maskedIP), md5.Sum([]byte("google")), t)
62 69
 	assertEqual(maxConc, 128, t)
63 70
 	assertEqual(maxWin, 256, t)
64 71
 }

+ 9
- 10
irc/connection_limits/throttler_test.go View File

@@ -4,7 +4,6 @@
4 4
 package connection_limits
5 5
 
6 6
 import (
7
-	"net"
8 7
 	"reflect"
9 8
 	"testing"
10 9
 	"time"
@@ -83,7 +82,7 @@ func makeTestThrottler(v4len, v6len int) *Limiter {
83 82
 
84 83
 func TestConnectionThrottle(t *testing.T) {
85 84
 	throttler := makeTestThrottler(32, 64)
86
-	addr := net.ParseIP("8.8.8.8")
85
+	addr := easyParseIP("8.8.8.8")
87 86
 
88 87
 	for i := 0; i < 3; i += 1 {
89 88
 		err := throttler.AddClient(addr)
@@ -97,14 +96,14 @@ func TestConnectionThrottleIPv6(t *testing.T) {
97 96
 	throttler := makeTestThrottler(32, 64)
98 97
 
99 98
 	var err error
100
-	err = throttler.AddClient(net.ParseIP("2001:0db8::1"))
99
+	err = throttler.AddClient(easyParseIP("2001:0db8::1"))
101 100
 	assertEqual(err, nil, t)
102
-	err = throttler.AddClient(net.ParseIP("2001:0db8::2"))
101
+	err = throttler.AddClient(easyParseIP("2001:0db8::2"))
103 102
 	assertEqual(err, nil, t)
104
-	err = throttler.AddClient(net.ParseIP("2001:0db8::3"))
103
+	err = throttler.AddClient(easyParseIP("2001:0db8::3"))
105 104
 	assertEqual(err, nil, t)
106 105
 
107
-	err = throttler.AddClient(net.ParseIP("2001:0db8::4"))
106
+	err = throttler.AddClient(easyParseIP("2001:0db8::4"))
108 107
 	assertEqual(err, ErrThrottleExceeded, t)
109 108
 }
110 109
 
@@ -112,13 +111,13 @@ func TestConnectionThrottleIPv4(t *testing.T) {
112 111
 	throttler := makeTestThrottler(24, 64)
113 112
 
114 113
 	var err error
115
-	err = throttler.AddClient(net.ParseIP("192.168.1.101"))
114
+	err = throttler.AddClient(easyParseIP("192.168.1.101"))
116 115
 	assertEqual(err, nil, t)
117
-	err = throttler.AddClient(net.ParseIP("192.168.1.102"))
116
+	err = throttler.AddClient(easyParseIP("192.168.1.102"))
118 117
 	assertEqual(err, nil, t)
119
-	err = throttler.AddClient(net.ParseIP("192.168.1.103"))
118
+	err = throttler.AddClient(easyParseIP("192.168.1.103"))
120 119
 	assertEqual(err, nil, t)
121 120
 
122
-	err = throttler.AddClient(net.ParseIP("192.168.1.104"))
121
+	err = throttler.AddClient(easyParseIP("192.168.1.104"))
123 122
 	assertEqual(err, ErrThrottleExceeded, t)
124 123
 }

+ 28
- 45
irc/dline.go View File

@@ -11,6 +11,7 @@ import (
11 11
 	"sync"
12 12
 	"time"
13 13
 
14
+	"github.com/oragono/oragono/irc/flatip"
14 15
 	"github.com/oragono/oragono/irc/utils"
15 16
 	"github.com/tidwall/buntdb"
16 17
 )
@@ -54,34 +55,22 @@ func (info IPBanInfo) BanMessage(message string) string {
54 55
 	return message
55 56
 }
56 57
 
57
-// dLineNet contains the net itself and expiration time for a given network.
58
-type dLineNet struct {
59
-	// Network is the network that is blocked.
60
-	// This is always an IPv6 CIDR; IPv4 CIDRs are translated with the 4-in-6 prefix,
61
-	// individual IPv4 and IPV6 addresses are translated to the relevant /128.
62
-	Network net.IPNet
63
-	// Info contains information on the ban.
64
-	Info IPBanInfo
65
-}
66
-
67 58
 // DLineManager manages and dlines.
68 59
 type DLineManager struct {
69 60
 	sync.RWMutex                // tier 1
70 61
 	persistenceMutex sync.Mutex // tier 2
71 62
 	// networks that are dlined:
72
-	// XXX: the keys of this map (which are also the database persistence keys)
73
-	// are the human-readable representations returned by NetToNormalizedString
74
-	networks map[string]dLineNet
63
+	networks map[flatip.IPNet]IPBanInfo
75 64
 	// this keeps track of expiration timers for temporary bans
76
-	expirationTimers map[string]*time.Timer
65
+	expirationTimers map[flatip.IPNet]*time.Timer
77 66
 	server           *Server
78 67
 }
79 68
 
80 69
 // NewDLineManager returns a new DLineManager.
81 70
 func NewDLineManager(server *Server) *DLineManager {
82 71
 	var dm DLineManager
83
-	dm.networks = make(map[string]dLineNet)
84
-	dm.expirationTimers = make(map[string]*time.Timer)
72
+	dm.networks = make(map[flatip.IPNet]IPBanInfo)
73
+	dm.expirationTimers = make(map[flatip.IPNet]*time.Timer)
85 74
 	dm.server = server
86 75
 
87 76
 	dm.loadFromDatastore()
@@ -96,9 +85,8 @@ func (dm *DLineManager) AllBans() map[string]IPBanInfo {
96 85
 	dm.RLock()
97 86
 	defer dm.RUnlock()
98 87
 
99
-	// map keys are already the human-readable forms, just return a copy of the map
100 88
 	for key, info := range dm.networks {
101
-		allb[key] = info.Info
89
+		allb[key.String()] = info
102 90
 	}
103 91
 
104 92
 	return allb
@@ -122,9 +110,9 @@ func (dm *DLineManager) AddNetwork(network net.IPNet, duration time.Duration, re
122 110
 	return dm.persistDline(id, info)
123 111
 }
124 112
 
125
-func (dm *DLineManager) addNetworkInternal(network net.IPNet, info IPBanInfo) (id string) {
126
-	network = utils.NormalizeNet(network)
127
-	id = utils.NetToNormalizedString(network)
113
+func (dm *DLineManager) addNetworkInternal(network net.IPNet, info IPBanInfo) (id flatip.IPNet) {
114
+	flatnet := flatip.FromNetIPNet(network)
115
+	id = flatnet
128 116
 
129 117
 	var timeLeft time.Duration
130 118
 	if info.Duration != 0 {
@@ -137,12 +125,9 @@ func (dm *DLineManager) addNetworkInternal(network net.IPNet, info IPBanInfo) (i
137 125
 	dm.Lock()
138 126
 	defer dm.Unlock()
139 127
 
140
-	dm.networks[id] = dLineNet{
141
-		Network: network,
142
-		Info:    info,
143
-	}
128
+	dm.networks[flatnet] = info
144 129
 
145
-	dm.cancelTimer(id)
130
+	dm.cancelTimer(flatnet)
146 131
 
147 132
 	if info.Duration == 0 {
148 133
 		return
@@ -154,29 +139,29 @@ func (dm *DLineManager) addNetworkInternal(network net.IPNet, info IPBanInfo) (i
154 139
 		dm.Lock()
155 140
 		defer dm.Unlock()
156 141
 
157
-		netBan, ok := dm.networks[id]
158
-		if ok && netBan.Info.TimeCreated.Equal(timeCreated) {
159
-			delete(dm.networks, id)
142
+		banInfo, ok := dm.networks[flatnet]
143
+		if ok && banInfo.TimeCreated.Equal(timeCreated) {
144
+			delete(dm.networks, flatnet)
160 145
 			// TODO(slingamn) here's where we'd remove it from the radix tree
161
-			delete(dm.expirationTimers, id)
146
+			delete(dm.expirationTimers, flatnet)
162 147
 		}
163 148
 	}
164
-	dm.expirationTimers[id] = time.AfterFunc(timeLeft, processExpiration)
149
+	dm.expirationTimers[flatnet] = time.AfterFunc(timeLeft, processExpiration)
165 150
 
166 151
 	return
167 152
 }
168 153
 
169
-func (dm *DLineManager) cancelTimer(id string) {
170
-	oldTimer := dm.expirationTimers[id]
154
+func (dm *DLineManager) cancelTimer(flatnet flatip.IPNet) {
155
+	oldTimer := dm.expirationTimers[flatnet]
171 156
 	if oldTimer != nil {
172 157
 		oldTimer.Stop()
173
-		delete(dm.expirationTimers, id)
158
+		delete(dm.expirationTimers, flatnet)
174 159
 	}
175 160
 }
176 161
 
177
-func (dm *DLineManager) persistDline(id string, info IPBanInfo) error {
162
+func (dm *DLineManager) persistDline(id flatip.IPNet, info IPBanInfo) error {
178 163
 	// save in datastore
179
-	dlineKey := fmt.Sprintf(keyDlineEntry, id)
164
+	dlineKey := fmt.Sprintf(keyDlineEntry, id.String())
180 165
 	// assemble json from ban info
181 166
 	b, err := json.Marshal(info)
182 167
 	if err != nil {
@@ -199,8 +184,8 @@ func (dm *DLineManager) persistDline(id string, info IPBanInfo) error {
199 184
 	return err
200 185
 }
201 186
 
202
-func (dm *DLineManager) unpersistDline(id string) error {
203
-	dlineKey := fmt.Sprintf(keyDlineEntry, id)
187
+func (dm *DLineManager) unpersistDline(id flatip.IPNet) error {
188
+	dlineKey := fmt.Sprintf(keyDlineEntry, id.String())
204 189
 	return dm.server.store.Update(func(tx *buntdb.Tx) error {
205 190
 		_, err := tx.Delete(dlineKey)
206 191
 		return err
@@ -212,7 +197,7 @@ func (dm *DLineManager) RemoveNetwork(network net.IPNet) error {
212 197
 	dm.persistenceMutex.Lock()
213 198
 	defer dm.persistenceMutex.Unlock()
214 199
 
215
-	id := utils.NetToNormalizedString(utils.NormalizeNet(network))
200
+	id := flatip.FromNetIPNet(network)
216 201
 
217 202
 	present := func() bool {
218 203
 		dm.Lock()
@@ -241,8 +226,7 @@ func (dm *DLineManager) RemoveIP(addr net.IP) error {
241 226
 }
242 227
 
243 228
 // CheckIP returns whether or not an IP address was banned, and how long it is banned for.
244
-func (dm *DLineManager) CheckIP(addr net.IP) (isBanned bool, info IPBanInfo) {
245
-	addr = addr.To16() // almost certainly unnecessary
229
+func (dm *DLineManager) CheckIP(addr flatip.IP) (isBanned bool, info IPBanInfo) {
246 230
 	if addr.IsLoopback() {
247 231
 		return // #671
248 232
 	}
@@ -252,13 +236,12 @@ func (dm *DLineManager) CheckIP(addr net.IP) (isBanned bool, info IPBanInfo) {
252 236
 
253 237
 	// check networks
254 238
 	// TODO(slingamn) use a radix tree as the data plane for this
255
-	for _, netBan := range dm.networks {
256
-		if netBan.Network.Contains(addr) {
257
-			return true, netBan.Info
239
+	for flatnet, info := range dm.networks {
240
+		if flatnet.Contains(addr) {
241
+			return true, info
258 242
 		}
259 243
 	}
260 244
 	// no matches!
261
-	isBanned = false
262 245
 	return
263 246
 }
264 247
 

+ 33
- 0
irc/flatip/adhoc.go View File

@@ -0,0 +1,33 @@
1
+// Copyright 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
2
+// Released under the MIT license
3
+
4
+package flatip
5
+
6
+// begin ad-hoc utilities
7
+
8
+// ParseToNormalizedNet attempts to interpret a string either as an IP
9
+// network in CIDR notation, returning an IPNet, or as an IP address,
10
+// returning an IPNet that contains only that address.
11
+func ParseToNormalizedNet(netstr string) (ipnet IPNet, err error) {
12
+	_, ipnet, err = ParseCIDR(netstr)
13
+	if err == nil {
14
+		return
15
+	}
16
+	ip, err := ParseIP(netstr)
17
+	if err == nil {
18
+		ipnet.IP = ip
19
+		ipnet.PrefixLen = 128
20
+	}
21
+	return
22
+}
23
+
24
+// IPInNets is a convenience function for testing whether an IP is contained
25
+// in any member of a slice of IPNet's.
26
+func IPInNets(addr IP, nets []IPNet) bool {
27
+	for _, net := range nets {
28
+		if net.Contains(addr) {
29
+			return true
30
+		}
31
+	}
32
+	return false
33
+}

+ 202
- 0
irc/flatip/flatip.go View File

@@ -0,0 +1,202 @@
1
+// Copyright 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
2
+// Copyright 2009 The Go Authors
3
+// Released under the MIT license
4
+
5
+package flatip
6
+
7
+import (
8
+	"bytes"
9
+	"errors"
10
+	"net"
11
+)
12
+
13
+var (
14
+	v4InV6Prefix = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff}
15
+
16
+	IPv6loopback = IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
17
+	IPv6zero     = IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
18
+	IPv4zero     = IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 0, 0, 0, 0}
19
+
20
+	ErrInvalidIPString = errors.New("String could not be interpreted as an IP address")
21
+)
22
+
23
+// packed versions of net.IP and net.IPNet; these are pure value types,
24
+// so they can be compared with == and used as map keys.
25
+
26
+// IP is a 128-bit representation of an IP address, using the 4-in-6 mapping
27
+// to represent IPv4 addresses.
28
+type IP [16]byte
29
+
30
+// IPNet is a IP network. In a valid value, all bits after PrefixLen are zeroes.
31
+type IPNet struct {
32
+	IP
33
+	PrefixLen uint8
34
+}
35
+
36
+// NetIP converts an IP into a net.IP.
37
+func (ip IP) NetIP() (result net.IP) {
38
+	result = make(net.IP, 16)
39
+	copy(result[:], ip[:])
40
+	return
41
+}
42
+
43
+// FromNetIP converts a net.IP into an IP.
44
+func FromNetIP(ip net.IP) (result IP) {
45
+	if len(ip) == 16 {
46
+		copy(result[:], ip[:])
47
+	} else {
48
+		result[10] = 0xff
49
+		result[11] = 0xff
50
+		copy(result[12:], ip[:])
51
+	}
52
+	return
53
+}
54
+
55
+// IPv4 returns the IP address representation of a.b.c.d
56
+func IPv4(a, b, c, d byte) (result IP) {
57
+	copy(result[:12], v4InV6Prefix)
58
+	result[12] = a
59
+	result[13] = b
60
+	result[14] = c
61
+	result[15] = d
62
+	return
63
+}
64
+
65
+// ParseIP parses a string representation of an IP address into an IP.
66
+// Unlike net.ParseIP, it returns an error instead of a zero value on failure,
67
+// since the zero value of `IP` is a representation of a valid IP (::0, the
68
+// IPv6 "unspecified address").
69
+func ParseIP(ipstr string) (ip IP, err error) {
70
+	// TODO reimplement this without net.ParseIP
71
+	netip := net.ParseIP(ipstr)
72
+	if netip == nil {
73
+		err = ErrInvalidIPString
74
+		return
75
+	}
76
+	netip = netip.To16()
77
+	copy(ip[:], netip)
78
+	return
79
+}
80
+
81
+// String returns the string representation of an IP
82
+func (ip IP) String() string {
83
+	// TODO reimplement this without using (net.IP).String()
84
+	return (net.IP)(ip[:]).String()
85
+}
86
+
87
+// IsIPv4 returns whether the IP is an IPv4 address.
88
+func (ip IP) IsIPv4() bool {
89
+	return bytes.Equal(ip[:12], v4InV6Prefix)
90
+}
91
+
92
+// IsLoopback returns whether the IP is a loopback address.
93
+func (ip IP) IsLoopback() bool {
94
+	if ip.IsIPv4() {
95
+		return ip[12] == 127
96
+	} else {
97
+		return ip == IPv6loopback
98
+	}
99
+}
100
+
101
+func (ip IP) IsUnspecified() bool {
102
+	return ip == IPv4zero || ip == IPv6zero
103
+}
104
+
105
+func rawCidrMask(length int) (m IP) {
106
+	n := uint(length)
107
+	for i := 0; i < 16; i++ {
108
+		if n >= 8 {
109
+			m[i] = 0xff
110
+			n -= 8
111
+			continue
112
+		}
113
+		m[i] = ^byte(0xff >> n)
114
+		return
115
+	}
116
+	return
117
+}
118
+
119
+func (ip IP) applyMask(mask IP) (result IP) {
120
+	for i := 0; i < 16; i += 1 {
121
+		result[i] = ip[i] & mask[i]
122
+	}
123
+	return
124
+}
125
+
126
+func cidrMask(ones, bits int) (result IP) {
127
+	switch bits {
128
+	case 32:
129
+		return rawCidrMask(96 + ones)
130
+	case 128:
131
+		return rawCidrMask(ones)
132
+	default:
133
+		return
134
+	}
135
+}
136
+
137
+// Mask returns the result of masking ip with the CIDR mask of
138
+// length 'ones', out of a total of 'bits' (which must be either
139
+// 32 for an IPv4 subnet or 128 for an IPv6 subnet).
140
+func (ip IP) Mask(ones, bits int) (result IP) {
141
+	return ip.applyMask(cidrMask(ones, bits))
142
+}
143
+
144
+// ToNetIPNet converts an IPNet into a net.IPNet.
145
+func (cidr IPNet) ToNetIPNet() (result net.IPNet) {
146
+	return net.IPNet{
147
+		IP:   cidr.IP.NetIP(),
148
+		Mask: net.CIDRMask(int(cidr.PrefixLen), 128),
149
+	}
150
+}
151
+
152
+// Contains retuns whether the network contains `ip`.
153
+func (cidr IPNet) Contains(ip IP) bool {
154
+	maskedIP := ip.Mask(int(cidr.PrefixLen), 128)
155
+	return cidr.IP == maskedIP
156
+}
157
+
158
+// FromNetIPnet converts a net.IPNet into an IPNet.
159
+func FromNetIPNet(network net.IPNet) (result IPNet) {
160
+	ones, _ := network.Mask.Size()
161
+	if len(network.IP) == 16 {
162
+		copy(result.IP[:], network.IP[:])
163
+	} else {
164
+		result.IP[10] = 0xff
165
+		result.IP[11] = 0xff
166
+		copy(result.IP[12:], network.IP[:])
167
+		ones += 96
168
+	}
169
+	// perform masking so that equal CIDRs are ==
170
+	result.IP = result.IP.Mask(ones, 128)
171
+	result.PrefixLen = uint8(ones)
172
+	return
173
+}
174
+
175
+// String returns a string representation of an IPNet.
176
+func (cidr IPNet) String() string {
177
+	ip := make(net.IP, 16)
178
+	copy(ip[:], cidr.IP[:])
179
+	ipnet := net.IPNet{
180
+		IP:   ip,
181
+		Mask: net.CIDRMask(int(cidr.PrefixLen), 128),
182
+	}
183
+	return ipnet.String()
184
+}
185
+
186
+// IsZero tests whether ipnet is the zero value of an IPNet, 0::0/0.
187
+// Although this is a valid subnet, it can still be used as a sentinel
188
+// value in some contexts.
189
+func (ipnet IPNet) IsZero() bool {
190
+	return ipnet == IPNet{}
191
+}
192
+
193
+// ParseCIDR parses a string representation of an IP network in CIDR notation,
194
+// then returns it as an IPNet (along with the original, unmasked address).
195
+func ParseCIDR(netstr string) (ip IP, ipnet IPNet, err error) {
196
+	// TODO reimplement this without net.ParseCIDR
197
+	nip, nipnet, err := net.ParseCIDR(netstr)
198
+	if err != nil {
199
+		return
200
+	}
201
+	return FromNetIP(nip), FromNetIPNet(*nipnet), nil
202
+}

+ 174
- 0
irc/flatip/flatip_test.go View File

@@ -0,0 +1,174 @@
1
+package flatip
2
+
3
+import (
4
+	"bytes"
5
+	"math/rand"
6
+	"net"
7
+	"testing"
8
+	"time"
9
+)
10
+
11
+func easyParseIP(ipstr string) (result net.IP) {
12
+	result = net.ParseIP(ipstr)
13
+	if result == nil {
14
+		panic(ipstr)
15
+	}
16
+	return
17
+}
18
+
19
+func easyParseFlat(ipstr string) (result IP) {
20
+	x := easyParseIP(ipstr)
21
+	return FromNetIP(x)
22
+}
23
+
24
+func easyParseIPNet(nipstr string) (result net.IPNet) {
25
+	_, nip, err := net.ParseCIDR(nipstr)
26
+	if err != nil {
27
+		panic(err)
28
+	}
29
+	return *nip
30
+}
31
+
32
+func TestBasic(t *testing.T) {
33
+	nip := easyParseIP("8.8.8.8")
34
+	flatip := FromNetIP(nip)
35
+	if flatip.String() != "8.8.8.8" {
36
+		t.Errorf("conversions don't work")
37
+	}
38
+}
39
+
40
+func TestLoopback(t *testing.T) {
41
+	localhost_v4 := easyParseFlat("127.0.0.1")
42
+	localhost_v4_again := easyParseFlat("127.2.3.4")
43
+	google := easyParseFlat("8.8.8.8")
44
+	loopback_v6 := easyParseFlat("::1")
45
+	google_v6 := easyParseFlat("2607:f8b0:4006:801::2004")
46
+
47
+	if !(localhost_v4.IsLoopback() && localhost_v4_again.IsLoopback() && loopback_v6.IsLoopback()) {
48
+		t.Errorf("can't detect loopbacks")
49
+	}
50
+
51
+	if google_v6.IsLoopback() || google.IsLoopback() {
52
+		t.Errorf("incorrectly detected loopbacks")
53
+	}
54
+}
55
+
56
+func TestContains(t *testing.T) {
57
+	nipnet := easyParseIPNet("8.8.0.0/16")
58
+	flatipnet := FromNetIPNet(nipnet)
59
+	nip := easyParseIP("8.8.8.8")
60
+	flatip_ := FromNetIP(nip)
61
+	if !flatipnet.Contains(flatip_) {
62
+		t.Errorf("contains doesn't work")
63
+	}
64
+}
65
+
66
+var testIPStrs = []string{
67
+	"8.8.8.8",
68
+	"127.0.0.1",
69
+	"1.1.1.1",
70
+	"128.127.65.64",
71
+	"2001:0db8::1",
72
+	"::1",
73
+	"255.255.255.255",
74
+}
75
+
76
+func doMaskingTest(ip net.IP, t *testing.T) {
77
+	flat := FromNetIP(ip)
78
+	netLen := len(ip) * 8
79
+	for i := 0; i < netLen; i++ {
80
+		masked := flat.Mask(i, netLen)
81
+		netMask := net.CIDRMask(i, netLen)
82
+		netMasked := ip.Mask(netMask)
83
+		if !bytes.Equal(masked[:], netMasked.To16()) {
84
+			t.Errorf("Masking %s with %d/%d; expected %s, got %s", ip.String(), i, netLen, netMasked.String(), masked.String())
85
+		}
86
+	}
87
+}
88
+
89
+func TestMasking(t *testing.T) {
90
+	for _, ipstr := range testIPStrs {
91
+		doMaskingTest(easyParseIP(ipstr), t)
92
+	}
93
+}
94
+
95
+func TestMaskingFuzz(t *testing.T) {
96
+	r := rand.New(rand.NewSource(time.Now().UnixNano()))
97
+	buf := make([]byte, 4)
98
+	for i := 0; i < 10000; i++ {
99
+		r.Read(buf)
100
+		doMaskingTest(net.IP(buf), t)
101
+	}
102
+
103
+	buf = make([]byte, 16)
104
+	for i := 0; i < 10000; i++ {
105
+		r.Read(buf)
106
+		doMaskingTest(net.IP(buf), t)
107
+	}
108
+}
109
+
110
+func BenchmarkMasking(b *testing.B) {
111
+	ip := easyParseIP("2001:0db8::42")
112
+	flat := FromNetIP(ip)
113
+	b.ResetTimer()
114
+
115
+	for i := 0; i < b.N; i++ {
116
+		flat.Mask(64, 128)
117
+	}
118
+}
119
+
120
+func BenchmarkMaskingLegacy(b *testing.B) {
121
+	ip := easyParseIP("2001:0db8::42")
122
+	mask := net.CIDRMask(64, 128)
123
+	b.ResetTimer()
124
+
125
+	for i := 0; i < b.N; i++ {
126
+		ip.Mask(mask)
127
+	}
128
+}
129
+
130
+func BenchmarkMaskingCached(b *testing.B) {
131
+	i := easyParseIP("2001:0db8::42")
132
+	flat := FromNetIP(i)
133
+	mask := cidrMask(64, 128)
134
+	b.ResetTimer()
135
+
136
+	for i := 0; i < b.N; i++ {
137
+		flat.applyMask(mask)
138
+	}
139
+}
140
+
141
+func BenchmarkMaskingConstruct(b *testing.B) {
142
+	for i := 0; i < b.N; i++ {
143
+		cidrMask(69, 128)
144
+	}
145
+}
146
+
147
+func BenchmarkContains(b *testing.B) {
148
+	ip := easyParseIP("2001:0db8::42")
149
+	flat := FromNetIP(ip)
150
+	_, ipnet, err := net.ParseCIDR("2001:0db8::/64")
151
+	if err != nil {
152
+		panic(err)
153
+	}
154
+	flatnet := FromNetIPNet(*ipnet)
155
+	b.ResetTimer()
156
+
157
+	for i := 0; i < b.N; i++ {
158
+		flatnet.Contains(flat)
159
+	}
160
+}
161
+
162
+func BenchmarkContainsLegacy(b *testing.B) {
163
+	ip := easyParseIP("2001:0db8::42")
164
+	_, ipnetptr, err := net.ParseCIDR("2001:0db8::/64")
165
+	if err != nil {
166
+		panic(err)
167
+	}
168
+	ipnet := *ipnetptr
169
+	b.ResetTimer()
170
+
171
+	for i := 0; i < b.N; i++ {
172
+		ipnet.Contains(ip)
173
+	}
174
+}

+ 2
- 1
irc/gateways.go View File

@@ -9,6 +9,7 @@ import (
9 9
 	"errors"
10 10
 	"net"
11 11
 
12
+	"github.com/oragono/oragono/irc/flatip"
12 13
 	"github.com/oragono/oragono/irc/modes"
13 14
 	"github.com/oragono/oragono/irc/utils"
14 15
 )
@@ -87,7 +88,7 @@ func (client *Client) ApplyProxiedIP(session *Session, proxiedIP net.IP, tls boo
87 88
 	}
88 89
 	// successfully added a limiter entry for the proxied IP;
89 90
 	// remove the entry for the real IP if applicable (#197)
90
-	client.server.connectionLimiter.RemoveClient(session.realIP)
91
+	client.server.connectionLimiter.RemoveClient(flatip.FromNetIP(session.realIP))
91 92
 
92 93
 	// given IP is sane! override the client's current IP
93 94
 	client.server.logger.Info("connect-ip", "Accepted proxy IP for client", proxiedIP.String())

+ 6
- 0
irc/handlers.go View File

@@ -24,6 +24,7 @@ import (
24 24
 	"github.com/goshuirc/irc-go/ircmsg"
25 25
 	"github.com/oragono/oragono/irc/caps"
26 26
 	"github.com/oragono/oragono/irc/custime"
27
+	"github.com/oragono/oragono/irc/flatip"
27 28
 	"github.com/oragono/oragono/irc/history"
28 29
 	"github.com/oragono/oragono/irc/jwt"
29 30
 	"github.com/oragono/oragono/irc/modes"
@@ -2798,6 +2799,11 @@ func unDLineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
2798 2799
 	// get host
2799 2800
 	hostString := msg.Params[0]
2800 2801
 
2802
+	// TODO(#1447) consolidate this into the "unban" command
2803
+	if flatip, ipErr := flatip.ParseIP(hostString); ipErr == nil {
2804
+		server.connectionLimiter.ResetThrottle(flatip)
2805
+	}
2806
+
2801 2807
 	// check host
2802 2808
 	hostNet, err := utils.NormalizedNetFromString(hostString)
2803 2809
 

+ 9
- 16
irc/server.go View File

@@ -23,6 +23,7 @@ import (
23 23
 
24 24
 	"github.com/oragono/oragono/irc/caps"
25 25
 	"github.com/oragono/oragono/irc/connection_limits"
26
+	"github.com/oragono/oragono/irc/flatip"
26 27
 	"github.com/oragono/oragono/irc/history"
27 28
 	"github.com/oragono/oragono/irc/logger"
28 29
 	"github.com/oragono/oragono/irc/modes"
@@ -160,31 +161,23 @@ func (server *Server) checkBans(config *Config, ipaddr net.IP, checkScripts bool
160 161
 		}
161 162
 	}
162 163
 
164
+	flat := flatip.FromNetIP(ipaddr)
165
+
163 166
 	// check DLINEs
164
-	isBanned, info := server.dlines.CheckIP(ipaddr)
167
+	isBanned, info := server.dlines.CheckIP(flat)
165 168
 	if isBanned {
166
-		server.logger.Info("connect-ip", fmt.Sprintf("Client from %v rejected by d-line", ipaddr))
169
+		server.logger.Info("connect-ip", "Client rejected by d-line", ipaddr.String())
167 170
 		return true, false, info.BanMessage("You are banned from this server (%s)")
168 171
 	}
169 172
 
170 173
 	// check connection limits
171
-	err := server.connectionLimiter.AddClient(ipaddr)
174
+	err := server.connectionLimiter.AddClient(flat)
172 175
 	if err == connection_limits.ErrLimitExceeded {
173 176
 		// too many connections from one client, tell the client and close the connection
174
-		server.logger.Info("connect-ip", fmt.Sprintf("Client from %v rejected for connection limit", ipaddr))
177
+		server.logger.Info("connect-ip", "Client rejected for connection limit", ipaddr.String())
175 178
 		return true, false, "Too many clients from your network"
176 179
 	} else if err == connection_limits.ErrThrottleExceeded {
177
-		duration := config.Server.IPLimits.BanDuration
178
-		if duration != 0 {
179
-			server.dlines.AddIP(ipaddr, duration, throttleMessage,
180
-				"Exceeded automated connection throttle", "auto.connection.throttler")
181
-			// they're DLINE'd for 15 minutes or whatever, so we can reset the connection throttle now,
182
-			// and once their temporary DLINE is finished they can fill up the throttler again
183
-			server.connectionLimiter.ResetThrottle(ipaddr)
184
-		}
185
-		server.logger.Info(
186
-			"connect-ip",
187
-			fmt.Sprintf("Client from %v exceeded connection throttle, d-lining for %v", ipaddr, duration))
180
+		server.logger.Info("connect-ip", "Client exceeded connection throttle", ipaddr.String())
188 181
 		return true, false, throttleMessage
189 182
 	} else if err != nil {
190 183
 		server.logger.Warning("internal", "unexpected ban result", err.Error())
@@ -211,7 +204,7 @@ func (server *Server) checkBans(config *Config, ipaddr net.IP, checkScripts bool
211 204
 		}
212 205
 		if output.Result == IPBanned {
213 206
 			// XXX roll back IP connection/throttling addition for the IP
214
-			server.connectionLimiter.RemoveClient(ipaddr)
207
+			server.connectionLimiter.RemoveClient(flat)
215 208
 			server.logger.Info("connect-ip", "Rejected client due to ip-check-script", ipaddr.String())
216 209
 			return true, false, output.BanMessage
217 210
 		} else if output.Result == IPRequireSASL {

+ 0
- 3
traditional.yaml View File

@@ -220,9 +220,6 @@ server:
220 220
         window: 10m
221 221
         # maximum number of new connections per IP/CIDR within the given duration
222 222
         max-connections-per-window: 32
223
-        # how long to ban offenders for. after banning them, the number of connections is
224
-        # reset, which lets you use /UNDLINE to unban people
225
-        throttle-ban-duration: 10m
226 223
 
227 224
         # how wide the CIDR should be for IPv4 (a /32 is a fully specified IPv4 address)
228 225
         cidr-len-ipv4: 32

Loading…
Cancel
Save