mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-10 22:19:31 +01:00
implement ip cloaking
This commit is contained in:
parent
585a6557a4
commit
c28e6d13f9
@ -70,6 +70,7 @@ type Client struct {
|
||||
preregNick string
|
||||
proxiedIP net.IP // actual remote IP if using the PROXY protocol
|
||||
rawHostname string
|
||||
cloakedHostname string
|
||||
realname string
|
||||
realIP net.IP
|
||||
registered bool
|
||||
@ -215,6 +216,7 @@ func RunNewClient(server *Server, conn clientConn) {
|
||||
session.realIP = utils.AddrToIP(remoteAddr)
|
||||
// set the hostname for this client (may be overridden later by PROXY or WEBIRC)
|
||||
session.rawHostname = utils.LookupHostname(session.realIP.String())
|
||||
client.cloakedHostname = config.Server.Cloaks.ComputeCloak(session.realIP)
|
||||
if utils.AddrIsLocal(remoteAddr) {
|
||||
// treat local connections as secure (may be overridden later by WEBIRC)
|
||||
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.
|
||||
func (client *Client) updateNickMaskNoMutex() {
|
||||
client.hostname = client.getVHostNoMutex()
|
||||
if client.hostname == "" {
|
||||
client.hostname = client.cloakedHostname
|
||||
if client.hostname == "" {
|
||||
client.hostname = client.rawHostname
|
||||
}
|
||||
}
|
||||
|
||||
cfhostname, err := Casefold(client.hostname)
|
||||
if err != nil {
|
||||
@ -831,6 +836,7 @@ func (client *Client) AllNickmasks() (masks []string) {
|
||||
nick := client.nickCasefolded
|
||||
username := client.username
|
||||
rawHostname := client.rawHostname
|
||||
cloakedHostname := client.cloakedHostname
|
||||
vhost := client.getVHostNoMutex()
|
||||
client.stateMutex.RUnlock()
|
||||
username = strings.ToLower(username)
|
||||
@ -849,6 +855,10 @@ func (client *Client) AllNickmasks() (masks []string) {
|
||||
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())
|
||||
if ipmask != rawhostmask {
|
||||
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"`
|
||||
}
|
||||
|
||||
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.
|
||||
type Config struct {
|
||||
Network struct {
|
||||
@ -297,6 +329,7 @@ type Config struct {
|
||||
isupport isupport.List
|
||||
ConnectionLimiter connection_limits.LimiterConfig `yaml:"connection-limits"`
|
||||
ConnectionThrottler connection_limits.ThrottlerConfig `yaml:"connection-throttling"`
|
||||
Cloaks CloakConfig `yaml:"ip-cloaking"`
|
||||
}
|
||||
|
||||
Languages struct {
|
||||
@ -728,6 +761,8 @@ func LoadConfig(filename string) (config *Config, err error) {
|
||||
config.History.ClientLength = 0
|
||||
}
|
||||
|
||||
config.Server.Cloaks.postprocess()
|
||||
|
||||
for _, listenAddress := range config.Server.TorListeners.Listeners {
|
||||
found := false
|
||||
for _, configuredListener := range config.Server.Listen {
|
||||
|
@ -70,6 +70,7 @@ func (client *Client) ApplyProxiedIP(session *Session, proxiedIP string, tls boo
|
||||
ipstring := parsedProxiedIP.String()
|
||||
client.server.logger.Info("localconnect-ip", "Accepted proxy IP for client", ipstring)
|
||||
rawHostname := utils.LookupHostname(ipstring)
|
||||
cloakedHostname := client.server.Config().Server.Cloaks.ComputeCloak(parsedProxiedIP)
|
||||
|
||||
client.stateMutex.Lock()
|
||||
defer client.stateMutex.Unlock()
|
||||
@ -77,6 +78,7 @@ func (client *Client) ApplyProxiedIP(session *Session, proxiedIP string, tls boo
|
||||
client.proxiedIP = parsedProxiedIP
|
||||
session.rawHostname = rawHostname
|
||||
client.rawHostname = rawHostname
|
||||
client.cloakedHostname = cloakedHostname
|
||||
// nickmask will be updated when the client completes registration
|
||||
// set tls info
|
||||
client.certfp = ""
|
||||
|
@ -21,6 +21,8 @@ import (
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/crypto/sha3"
|
||||
|
||||
"github.com/goshuirc/irc-go/ircfmt"
|
||||
"github.com/oragono/oragono/irc/caps"
|
||||
"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
|
||||
//
|
||||
|
@ -11,7 +11,7 @@ import (
|
||||
|
||||
var (
|
||||
// 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 (
|
||||
@ -24,7 +24,7 @@ func GenerateSecretToken() string {
|
||||
var buf [16]byte
|
||||
rand.Read(buf[:])
|
||||
// 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
|
||||
|
39
oragono.yaml
39
oragono.yaml
@ -188,6 +188,45 @@ server:
|
||||
# - "192.168.1.1"
|
||||
# - "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
|
||||
accounts:
|
||||
# account registration
|
||||
|
Loading…
Reference in New Issue
Block a user