mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-26 05:49:25 +01:00
implement ip cloaking
This commit is contained in:
parent
585a6557a4
commit
c28e6d13f9
@ -70,6 +70,7 @@ type Client struct {
|
|||||||
preregNick string
|
preregNick string
|
||||||
proxiedIP net.IP // actual remote IP if using the PROXY protocol
|
proxiedIP net.IP // actual remote IP if using the PROXY protocol
|
||||||
rawHostname string
|
rawHostname string
|
||||||
|
cloakedHostname string
|
||||||
realname string
|
realname string
|
||||||
realIP net.IP
|
realIP net.IP
|
||||||
registered bool
|
registered bool
|
||||||
@ -215,6 +216,7 @@ func RunNewClient(server *Server, conn clientConn) {
|
|||||||
session.realIP = utils.AddrToIP(remoteAddr)
|
session.realIP = utils.AddrToIP(remoteAddr)
|
||||||
// set the hostname for this client (may be overridden later by PROXY or WEBIRC)
|
// set the hostname for this client (may be overridden later by PROXY or WEBIRC)
|
||||||
session.rawHostname = utils.LookupHostname(session.realIP.String())
|
session.rawHostname = utils.LookupHostname(session.realIP.String())
|
||||||
|
client.cloakedHostname = config.Server.Cloaks.ComputeCloak(session.realIP)
|
||||||
if utils.AddrIsLocal(remoteAddr) {
|
if utils.AddrIsLocal(remoteAddr) {
|
||||||
// treat local connections as secure (may be overridden later by WEBIRC)
|
// treat local connections as secure (may be overridden later by WEBIRC)
|
||||||
client.SetMode(modes.TLS, true)
|
client.SetMode(modes.TLS, true)
|
||||||
@ -811,9 +813,12 @@ func (client *Client) updateNick(nick, nickCasefolded, skeleton string) {
|
|||||||
// updateNickMaskNoMutex updates the casefolded nickname and nickmask, not acquiring any mutexes.
|
// updateNickMaskNoMutex updates the casefolded nickname and nickmask, not acquiring any mutexes.
|
||||||
func (client *Client) updateNickMaskNoMutex() {
|
func (client *Client) updateNickMaskNoMutex() {
|
||||||
client.hostname = client.getVHostNoMutex()
|
client.hostname = client.getVHostNoMutex()
|
||||||
|
if client.hostname == "" {
|
||||||
|
client.hostname = client.cloakedHostname
|
||||||
if client.hostname == "" {
|
if client.hostname == "" {
|
||||||
client.hostname = client.rawHostname
|
client.hostname = client.rawHostname
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cfhostname, err := Casefold(client.hostname)
|
cfhostname, err := Casefold(client.hostname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -831,6 +836,7 @@ func (client *Client) AllNickmasks() (masks []string) {
|
|||||||
nick := client.nickCasefolded
|
nick := client.nickCasefolded
|
||||||
username := client.username
|
username := client.username
|
||||||
rawHostname := client.rawHostname
|
rawHostname := client.rawHostname
|
||||||
|
cloakedHostname := client.cloakedHostname
|
||||||
vhost := client.getVHostNoMutex()
|
vhost := client.getVHostNoMutex()
|
||||||
client.stateMutex.RUnlock()
|
client.stateMutex.RUnlock()
|
||||||
username = strings.ToLower(username)
|
username = strings.ToLower(username)
|
||||||
@ -849,6 +855,10 @@ func (client *Client) AllNickmasks() (masks []string) {
|
|||||||
masks = append(masks, rawhostmask)
|
masks = append(masks, rawhostmask)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cloakedHostname != "" {
|
||||||
|
masks = append(masks, fmt.Sprintf("%s!%s@%s", nick, username, cloakedHostname))
|
||||||
|
}
|
||||||
|
|
||||||
ipmask := fmt.Sprintf("%s!%s@%s", nick, username, client.IPString())
|
ipmask := fmt.Sprintf("%s!%s@%s", nick, username, client.IPString())
|
||||||
if ipmask != rawhostmask {
|
if ipmask != rawhostmask {
|
||||||
masks = append(masks, ipmask)
|
masks = append(masks, ipmask)
|
||||||
|
99
irc/cloak_test.go
Normal file
99
irc/cloak_test.go
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
// Copyright (c) 2019 Shivaram Lingamneni
|
||||||
|
// released under the MIT license
|
||||||
|
|
||||||
|
package irc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func easyParseIP(ipstr string) (result net.IP) {
|
||||||
|
result = net.ParseIP(ipstr)
|
||||||
|
if result == nil {
|
||||||
|
panic(ipstr)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloakConfForTesting() CloakConfig {
|
||||||
|
config := CloakConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Netname: "oragono",
|
||||||
|
Secret: "_BdVPWB5sray7McbFmeuJL996yaLgG4l9tEyficGXKg",
|
||||||
|
CidrLenIPv4: 32,
|
||||||
|
CidrLenIPv6: 64,
|
||||||
|
NumBits: 80,
|
||||||
|
}
|
||||||
|
config.postprocess()
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCloakDeterminism(t *testing.T) {
|
||||||
|
config := cloakConfForTesting()
|
||||||
|
|
||||||
|
v4ip := easyParseIP("8.8.8.8").To4()
|
||||||
|
assertEqual(config.ComputeCloak(v4ip), "d2z5guriqhzwazyr.oragono", t)
|
||||||
|
// use of the 4-in-6 mapping should not affect the cloak
|
||||||
|
v6mappedIP := v4ip.To16()
|
||||||
|
assertEqual(config.ComputeCloak(v6mappedIP), "d2z5guriqhzwazyr.oragono", t)
|
||||||
|
|
||||||
|
v6ip := easyParseIP("2001:0db8::1")
|
||||||
|
assertEqual(config.ComputeCloak(v6ip), "w7ren6nxii6f3i3d.oragono", t)
|
||||||
|
// same CIDR, so same cloak:
|
||||||
|
v6ipsamecidr := easyParseIP("2001:0db8::2")
|
||||||
|
assertEqual(config.ComputeCloak(v6ipsamecidr), "w7ren6nxii6f3i3d.oragono", t)
|
||||||
|
v6ipdifferentcidr := easyParseIP("2001:0db9::1")
|
||||||
|
// different CIDR, different cloak:
|
||||||
|
assertEqual(config.ComputeCloak(v6ipdifferentcidr), "ccmptyrjwsxv4f4d.oragono", t)
|
||||||
|
|
||||||
|
// cloak values must be sensitive to changes in the secret key
|
||||||
|
config.Secret = "HJcXK4lLawxBE4-9SIdPji_21YiL3N5r5f5-SPNrGVY"
|
||||||
|
assertEqual(config.ComputeCloak(v4ip), "4khy3usk8mfu42pe.oragono", t)
|
||||||
|
assertEqual(config.ComputeCloak(v6mappedIP), "4khy3usk8mfu42pe.oragono", t)
|
||||||
|
assertEqual(config.ComputeCloak(v6ip), "mxpk3c83vdxkek9j.oragono", t)
|
||||||
|
assertEqual(config.ComputeCloak(v6ipsamecidr), "mxpk3c83vdxkek9j.oragono", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCloakShortv4Cidr(t *testing.T) {
|
||||||
|
config := CloakConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Netname: "oragono",
|
||||||
|
Secret: "_BdVPWB5sray7McbFmeuJL996yaLgG4l9tEyficGXKg",
|
||||||
|
CidrLenIPv4: 24,
|
||||||
|
CidrLenIPv6: 64,
|
||||||
|
NumBits: 60,
|
||||||
|
}
|
||||||
|
config.postprocess()
|
||||||
|
|
||||||
|
v4ip := easyParseIP("8.8.8.8")
|
||||||
|
assertEqual(config.ComputeCloak(v4ip), "3cay3zc72tnui.oragono", t)
|
||||||
|
v4ipsamecidr := easyParseIP("8.8.8.9")
|
||||||
|
assertEqual(config.ComputeCloak(v4ipsamecidr), "3cay3zc72tnui.oragono", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCloakZeroBits(t *testing.T) {
|
||||||
|
config := cloakConfForTesting()
|
||||||
|
config.NumBits = 0
|
||||||
|
config.Netname = "example.com"
|
||||||
|
config.postprocess()
|
||||||
|
|
||||||
|
v4ip := easyParseIP("8.8.8.8").To4()
|
||||||
|
assertEqual(config.ComputeCloak(v4ip), "example.com", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCloakDisabled(t *testing.T) {
|
||||||
|
config := cloakConfForTesting()
|
||||||
|
config.Enabled = false
|
||||||
|
v4ip := easyParseIP("8.8.8.8").To4()
|
||||||
|
assertEqual(config.ComputeCloak(v4ip), "", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkCloaks(b *testing.B) {
|
||||||
|
config := cloakConfForTesting()
|
||||||
|
v6ip := easyParseIP("2001:0db8::1")
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
config.ComputeCloak(v6ip)
|
||||||
|
}
|
||||||
|
}
|
@ -263,6 +263,38 @@ type TorListenersConfig struct {
|
|||||||
MaxConnectionsPerDuration int `yaml:"max-connections-per-duration"`
|
MaxConnectionsPerDuration int `yaml:"max-connections-per-duration"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CloakConfig struct {
|
||||||
|
Enabled bool
|
||||||
|
Netname string
|
||||||
|
Secret string
|
||||||
|
CidrLenIPv4 int `yaml:"cidr-len-ipv4"`
|
||||||
|
CidrLenIPv6 int `yaml:"cidr-len-ipv6"`
|
||||||
|
NumBits int `yaml:"num-bits"`
|
||||||
|
|
||||||
|
numBytes int
|
||||||
|
ipv4Mask net.IPMask
|
||||||
|
ipv6Mask net.IPMask
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cloakConfig *CloakConfig) postprocess() {
|
||||||
|
// sanity checks:
|
||||||
|
numBits := cloakConfig.NumBits
|
||||||
|
if 0 == numBits {
|
||||||
|
numBits = 80
|
||||||
|
} else if 256 < numBits {
|
||||||
|
numBits = 256
|
||||||
|
}
|
||||||
|
|
||||||
|
// derived values:
|
||||||
|
cloakConfig.numBytes = numBits / 8
|
||||||
|
// round up to the nearest byte
|
||||||
|
if numBits%8 != 0 {
|
||||||
|
cloakConfig.numBytes += 1
|
||||||
|
}
|
||||||
|
cloakConfig.ipv4Mask = net.CIDRMask(cloakConfig.CidrLenIPv4, 32)
|
||||||
|
cloakConfig.ipv6Mask = net.CIDRMask(cloakConfig.CidrLenIPv6, 128)
|
||||||
|
}
|
||||||
|
|
||||||
// Config defines the overall configuration.
|
// Config defines the overall configuration.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Network struct {
|
Network struct {
|
||||||
@ -297,6 +329,7 @@ type Config struct {
|
|||||||
isupport isupport.List
|
isupport isupport.List
|
||||||
ConnectionLimiter connection_limits.LimiterConfig `yaml:"connection-limits"`
|
ConnectionLimiter connection_limits.LimiterConfig `yaml:"connection-limits"`
|
||||||
ConnectionThrottler connection_limits.ThrottlerConfig `yaml:"connection-throttling"`
|
ConnectionThrottler connection_limits.ThrottlerConfig `yaml:"connection-throttling"`
|
||||||
|
Cloaks CloakConfig `yaml:"ip-cloaking"`
|
||||||
}
|
}
|
||||||
|
|
||||||
Languages struct {
|
Languages struct {
|
||||||
@ -728,6 +761,8 @@ func LoadConfig(filename string) (config *Config, err error) {
|
|||||||
config.History.ClientLength = 0
|
config.History.ClientLength = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.Server.Cloaks.postprocess()
|
||||||
|
|
||||||
for _, listenAddress := range config.Server.TorListeners.Listeners {
|
for _, listenAddress := range config.Server.TorListeners.Listeners {
|
||||||
found := false
|
found := false
|
||||||
for _, configuredListener := range config.Server.Listen {
|
for _, configuredListener := range config.Server.Listen {
|
||||||
|
@ -70,6 +70,7 @@ func (client *Client) ApplyProxiedIP(session *Session, proxiedIP string, tls boo
|
|||||||
ipstring := parsedProxiedIP.String()
|
ipstring := parsedProxiedIP.String()
|
||||||
client.server.logger.Info("localconnect-ip", "Accepted proxy IP for client", ipstring)
|
client.server.logger.Info("localconnect-ip", "Accepted proxy IP for client", ipstring)
|
||||||
rawHostname := utils.LookupHostname(ipstring)
|
rawHostname := utils.LookupHostname(ipstring)
|
||||||
|
cloakedHostname := client.server.Config().Server.Cloaks.ComputeCloak(parsedProxiedIP)
|
||||||
|
|
||||||
client.stateMutex.Lock()
|
client.stateMutex.Lock()
|
||||||
defer client.stateMutex.Unlock()
|
defer client.stateMutex.Unlock()
|
||||||
@ -77,6 +78,7 @@ func (client *Client) ApplyProxiedIP(session *Session, proxiedIP string, tls boo
|
|||||||
client.proxiedIP = parsedProxiedIP
|
client.proxiedIP = parsedProxiedIP
|
||||||
session.rawHostname = rawHostname
|
session.rawHostname = rawHostname
|
||||||
client.rawHostname = rawHostname
|
client.rawHostname = rawHostname
|
||||||
|
client.cloakedHostname = cloakedHostname
|
||||||
// nickmask will be updated when the client completes registration
|
// nickmask will be updated when the client completes registration
|
||||||
// set tls info
|
// set tls info
|
||||||
client.certfp = ""
|
client.certfp = ""
|
||||||
|
@ -21,6 +21,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/sha3"
|
||||||
|
|
||||||
"github.com/goshuirc/irc-go/ircfmt"
|
"github.com/goshuirc/irc-go/ircfmt"
|
||||||
"github.com/oragono/oragono/irc/caps"
|
"github.com/oragono/oragono/irc/caps"
|
||||||
"github.com/oragono/oragono/irc/connection_limits"
|
"github.com/oragono/oragono/irc/connection_limits"
|
||||||
@ -283,6 +285,32 @@ func (server *Server) checkTorLimits() (banned bool, message string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// simple cloaking algorithm: normalize the IP to its CIDR,
|
||||||
|
// then hash the resulting bytes with a secret key,
|
||||||
|
// then truncate to the desired length, b32encode, and append the fake TLD.
|
||||||
|
func (config *CloakConfig) ComputeCloak(ip net.IP) string {
|
||||||
|
if !config.Enabled {
|
||||||
|
return ""
|
||||||
|
} else if config.NumBits == 0 {
|
||||||
|
return config.Netname
|
||||||
|
}
|
||||||
|
var masked net.IP
|
||||||
|
v4ip := ip.To4()
|
||||||
|
if v4ip != nil {
|
||||||
|
masked = v4ip.Mask(config.ipv4Mask)
|
||||||
|
} else {
|
||||||
|
masked = ip.Mask(config.ipv6Mask)
|
||||||
|
}
|
||||||
|
// SHA3(K || M):
|
||||||
|
// https://crypto.stackexchange.com/questions/17735/is-hmac-needed-for-a-sha-3-based-mac
|
||||||
|
input := make([]byte, len(config.Secret)+len(masked))
|
||||||
|
copy(input, config.Secret[:])
|
||||||
|
copy(input[len(config.Secret):], masked)
|
||||||
|
digest := sha3.Sum512(input)
|
||||||
|
b32digest := utils.B32Encoder.EncodeToString(digest[:config.numBytes])
|
||||||
|
return fmt.Sprintf("%s.%s", b32digest, config.Netname)
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// IRC protocol listeners
|
// IRC protocol listeners
|
||||||
//
|
//
|
||||||
|
@ -11,7 +11,7 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
// slingamn's own private b32 alphabet, removing 1, l, o, and 0
|
// slingamn's own private b32 alphabet, removing 1, l, o, and 0
|
||||||
b32encoder = base32.NewEncoding("abcdefghijkmnpqrstuvwxyz23456789").WithPadding(base32.NoPadding)
|
B32Encoder = base32.NewEncoding("abcdefghijkmnpqrstuvwxyz23456789").WithPadding(base32.NoPadding)
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -24,7 +24,7 @@ func GenerateSecretToken() string {
|
|||||||
var buf [16]byte
|
var buf [16]byte
|
||||||
rand.Read(buf[:])
|
rand.Read(buf[:])
|
||||||
// 26 ASCII characters, should be fine for most purposes
|
// 26 ASCII characters, should be fine for most purposes
|
||||||
return b32encoder.EncodeToString(buf[:])
|
return B32Encoder.EncodeToString(buf[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
// securely check if a supplied token matches a stored token
|
// securely check if a supplied token matches a stored token
|
||||||
|
39
oragono.yaml
39
oragono.yaml
@ -188,6 +188,45 @@ server:
|
|||||||
# - "192.168.1.1"
|
# - "192.168.1.1"
|
||||||
# - "2001:0db8::/32"
|
# - "2001:0db8::/32"
|
||||||
|
|
||||||
|
# IP cloaking hides users' IP addresses from other users and from channel admins
|
||||||
|
# (but not from server admins), while still allowing channel admins to ban
|
||||||
|
# offending IP addresses or networks. In place of hostnames derived from reverse
|
||||||
|
# DNS, users see fake domain names like pwbs2ui4377257x8.oragono. These names are
|
||||||
|
# generated deterministically from the underlying IP address, but if the underlying
|
||||||
|
# IP is not already known, it is infeasible to recover it from the cloaked name.
|
||||||
|
ip-cloaking:
|
||||||
|
# whether to enable IP cloaking
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
# fake TLD at the end of the hostname, e.g., pwbs2ui4377257x8.oragono
|
||||||
|
netname: "oragono"
|
||||||
|
|
||||||
|
# secret key to prevent dictionary attacks against cloaked IPs
|
||||||
|
# any high-entropy secret is valid for this purpose:
|
||||||
|
# you MUST generate a new one for your installation.
|
||||||
|
# suggestion: use the output of this command:
|
||||||
|
# python3 -c "import secrets; print(secrets.token_urlsafe())"
|
||||||
|
# note that rotating this key will invalidate all existing ban masks.
|
||||||
|
secret: "siaELnk6Kaeo65K3RCrwJjlWaZ-Bt3WuZ2L8MXLbNb4"
|
||||||
|
|
||||||
|
# the cloaked hostname is derived only from the CIDR (most significant bits
|
||||||
|
# of the IP address), up to a configurable number of bits. this is the
|
||||||
|
# granularity at which bans will take effect for ipv4 (a /32 is a fully
|
||||||
|
# specified IP address). note that changing this value will invalidate
|
||||||
|
# any stored bans.
|
||||||
|
cidr-len-ipv4: 32
|
||||||
|
|
||||||
|
# analogous value for ipv6 (an ipv6 /64 is the typical prefix assigned
|
||||||
|
# by an ISP to an individual customer for their LAN)
|
||||||
|
cidr-len-ipv6: 64
|
||||||
|
|
||||||
|
# number of bits of hash output to include in the cloaked hostname.
|
||||||
|
# more bits means less likelihood of distinct IPs colliding,
|
||||||
|
# at the cost of a longer cloaked hostname. if this value is set to 0,
|
||||||
|
# all users will receive simply `netname` as their cloaked hostname.
|
||||||
|
num-bits: 80
|
||||||
|
|
||||||
|
|
||||||
# account options
|
# account options
|
||||||
accounts:
|
accounts:
|
||||||
# account registration
|
# account registration
|
||||||
|
Loading…
Reference in New Issue
Block a user