From e2b55bfb548ed5c5702766350943f5a12e17e954 Mon Sep 17 00:00:00 2001 From: Leigh MacDonald Date: Wed, 17 Apr 2024 16:59:23 -0600 Subject: [PATCH] Add ban evasion whitelist support --- frontend/src/api/bans.ts | 3 ++ frontend/src/component/TopBar.tsx | 44 +++++++++++-------- .../src/component/formik/EvadeOKField.tsx | 29 ++++++++++++ .../component/formik/IncludeFriendsField.tsx | 7 ++- .../src/component/modal/BanSteamModal.tsx | 8 +++- .../src/component/table/BanSteamTable.tsx | 11 +++++ frontend/src/page/AdminVotesPage.tsx | 28 +++++++----- internal/ban/ban_steam_repository.go | 23 +++++----- internal/ban/ban_steam_service.go | 5 ++- internal/ban/ban_steam_usecase.go | 10 ++++- internal/chat/chat_usecase.go | 2 +- .../migrations/000084_ban_whitelist.down.sql | 6 +++ .../migrations/000084_ban_whitelist.up.sql | 6 +++ internal/discord/discord_service.go | 4 +- internal/domain/ban.go | 4 +- internal/srcds/srcds_service.go | 2 +- 16 files changed, 140 insertions(+), 52 deletions(-) create mode 100644 frontend/src/component/formik/EvadeOKField.tsx create mode 100644 internal/database/migrations/000084_ban_whitelist.down.sql create mode 100644 internal/database/migrations/000084_ban_whitelist.up.sql diff --git a/frontend/src/api/bans.ts b/frontend/src/api/bans.ts index d78bfdf8..1fe12523 100644 --- a/frontend/src/api/bans.ts +++ b/frontend/src/api/bans.ts @@ -175,6 +175,7 @@ export interface SteamBanRecord extends BanBase { report_id: number; ban_type: BanType; include_friends: boolean; + evade_ok: boolean; } export interface GroupBanRecord extends BanBase { @@ -212,6 +213,7 @@ interface BanReasonPayload { export interface BanPayloadSteam extends BanBasePayload, BanReasonPayload { report_id?: number; include_friends: boolean; + evade_ok: boolean; ban_type: BanType; } @@ -309,6 +311,7 @@ export const apiUpdateBanSteam = async ( ban_id: number, payload: UpdateBanPayload & { include_friends: boolean; + evade_ok: boolean; ban_type: BanType; } ) => diff --git a/frontend/src/component/TopBar.tsx b/frontend/src/component/TopBar.tsx index ec91bd54..3f3d15a4 100644 --- a/frontend/src/component/TopBar.tsx +++ b/frontend/src/component/TopBar.tsx @@ -2,6 +2,7 @@ import { JSX, useMemo, useState, MouseEvent } from 'react'; import { Link as RouterLink } from 'react-router-dom'; import AccountCircleIcon from '@mui/icons-material/AccountCircle'; import ArticleIcon from '@mui/icons-material/Article'; +import BlockIcon from '@mui/icons-material/Block'; import CellTowerIcon from '@mui/icons-material/CellTower'; import DarkModeIcon from '@mui/icons-material/DarkMode'; import DashboardIcon from '@mui/icons-material/Dashboard'; @@ -178,25 +179,32 @@ export const TopBar = () => { leftIcon: }, { - href: '/admin/ban/steam', - label: 'Ban Steam', - leftIcon: - }, - { - href: '/admin/ban/cidr', - label: 'Ban CIDR', - leftIcon: - }, - { - href: '/admin/ban/group', - label: 'Ban Steam Group', - leftIcon: - }, - { - href: '/admin/ban/asn', - label: 'Ban ASN', - leftIcon: + label: 'Ban', + leftIcon: , + items: [ + { + href: '/admin/ban/steam', + label: 'Steam', + leftIcon: + }, + { + href: '/admin/ban/cidr', + label: 'IP/CIDR', + leftIcon: + }, + { + href: '/admin/ban/group', + label: 'Steam Group', + leftIcon: + }, + { + href: '/admin/ban/asn', + label: 'ASN', + leftIcon: + } + ] }, + { href: '/admin/reports', label: 'Reports', diff --git a/frontend/src/component/formik/EvadeOKField.tsx b/frontend/src/component/formik/EvadeOKField.tsx new file mode 100644 index 00000000..5f3eb027 --- /dev/null +++ b/frontend/src/component/formik/EvadeOKField.tsx @@ -0,0 +1,29 @@ +import Checkbox from '@mui/material/Checkbox'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import FormGroup from '@mui/material/FormGroup'; +import Tooltip from '@mui/material/Tooltip'; +import { useFormikContext } from 'formik'; + +interface EvadeOKFieldValue { + evade_ok: boolean; +} + +export const EvadeOKField = () => { + const { values, handleChange } = useFormikContext(); + return ( + + + } + label="Evade OK" + name={'evade_ok'} + onChange={handleChange} + /> + + + ); +}; diff --git a/frontend/src/component/formik/IncludeFriendsField.tsx b/frontend/src/component/formik/IncludeFriendsField.tsx index 6311c547..fd7898fe 100644 --- a/frontend/src/component/formik/IncludeFriendsField.tsx +++ b/frontend/src/component/formik/IncludeFriendsField.tsx @@ -8,10 +8,9 @@ interface IncludeFriendsFieldValue { include_friends: boolean; } -export const IncludeFriendsField = () => { - const { values, handleChange } = useFormikContext< - T & IncludeFriendsFieldValue - >(); +export const IncludeFriendsField = () => { + const { values, handleChange } = + useFormikContext(); return ( + diff --git a/frontend/src/component/table/BanSteamTable.tsx b/frontend/src/component/table/BanSteamTable.tsx index 878b7624..5b367496 100644 --- a/frontend/src/component/table/BanSteamTable.tsx +++ b/frontend/src/component/table/BanSteamTable.tsx @@ -349,6 +349,17 @@ export const BanSteamTable = ({ newBans }: { newBans: SteamBanRecord[] }) => { /> ) }, + { + label: 'E', + tooltip: + 'Are othere players allowed to play from the same ip when a ban on that ip is active (eg. banned roomate/family)', + align: 'center', + width: '50px', + sortKey: 'evade_ok', + renderer: (row) => ( + + ) + }, { label: 'A', tooltip: diff --git a/frontend/src/page/AdminVotesPage.tsx b/frontend/src/page/AdminVotesPage.tsx index 5622644c..c3643bbb 100644 --- a/frontend/src/page/AdminVotesPage.tsx +++ b/frontend/src/page/AdminVotesPage.tsx @@ -1,11 +1,13 @@ import { ChangeEvent, useCallback } from 'react'; import useUrlState from '@ahooksjs/use-url-state'; +import FilterListIcon from '@mui/icons-material/FilterList'; import HowToVoteIcon from '@mui/icons-material/HowToVote'; import Stack from '@mui/material/Stack'; import Grid from '@mui/material/Unstable_Grid2'; import { Formik } from 'formik'; import { FormikHelpers } from 'formik/dist/types'; import { VoteResult } from '../api/votes.ts'; +import { ContainerWithHeader } from '../component/ContainerWithHeader.tsx'; import { ContainerWithHeaderAndButtons } from '../component/ContainerWithHeaderAndButtons'; import { FilterButtons } from '../component/formik/FilterButtons.tsx'; import { SourceIDField } from '../component/formik/SourceIDField.tsx'; @@ -85,18 +87,22 @@ export const AdminVotesPage = () => { onSubmit={onSubmit} > - - - - - - - - - + } + > + + + + + + + + + + - - + } diff --git a/internal/ban/ban_steam_repository.go b/internal/ban/ban_steam_repository.go index 5a79198f..159d2aa2 100644 --- a/internal/ban/ban_steam_repository.go +++ b/internal/ban/ban_steam_repository.go @@ -79,7 +79,7 @@ func (r *banSteamRepository) getBanByColumn(ctx context.Context, column string, Builder(). Select( "b.ban_id", "b.target_id", "b.source_id", "b.ban_type", "b.reason", - "b.reason_text", "b.note", "b.origin", "b.valid_until", "b.created_on", "b.updated_on", "b.include_friends", + "b.reason_text", "b.note", "b.origin", "b.valid_until", "b.created_on", "b.updated_on", "b.include_friends", "b.evade_ok", "b.deleted", "case WHEN b.report_id is null THEN 0 ELSE b.report_id END", "b.unban_reason_text", "b.is_enabled", "b.appeal_state", "b.last_ip", "s.personaname as source_personaname", "s.avatarhash", @@ -105,7 +105,7 @@ func (r *banSteamRepository) getBanByColumn(ctx context.Context, column string, if errScan := row. Scan(&person.BanID, &targetID, &sourceID, &person.BanType, &person.Reason, &person.ReasonText, &person.Note, &person.Origin, &person.ValidUntil, &person.CreatedOn, - &person.UpdatedOn, &person.IncludeFriends, &person.Deleted, &person.ReportID, &person.UnbanReasonText, + &person.UpdatedOn, &person.IncludeFriends, &person.EvadeOk, &person.Deleted, &person.ReportID, &person.UnbanReasonText, &person.IsEnabled, &person.AppealState, &person.LastIP, &person.SourceTarget.SourcePersonaname, &person.SourceTarget.SourceAvatarhash, &person.SourceTarget.TargetPersonaname, &person.SourceTarget.TargetAvatarhash, @@ -173,14 +173,14 @@ func (r *banSteamRepository) Save(ctx context.Context, ban *domain.BanSteam) err func (r *banSteamRepository) insertBan(ctx context.Context, ban *domain.BanSteam) error { const query = ` INSERT INTO ban (target_id, source_id, ban_type, reason, reason_text, note, valid_until, - created_on, updated_on, origin, report_id, appeal_state, include_friends, last_ip) + created_on, updated_on, origin, report_id, appeal_state, include_friends, evade_ok, last_ip) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, case WHEN $11 = 0 THEN null ELSE $11 END, $12, $13, $14) RETURNING ban_id` errQuery := r.db. QueryRow(ctx, query, ban.TargetID.Int64(), ban.SourceID.Int64(), ban.BanType, ban.Reason, ban.ReasonText, ban.Note, ban.ValidUntil, ban.CreatedOn, ban.UpdatedOn, ban.Origin, ban.ReportID, ban.AppealState, - ban.IncludeFriends, &ban.LastIP). + ban.IncludeFriends, ban.EvadeOk, &ban.LastIP). Scan(&ban.BanID) if errQuery != nil { @@ -214,6 +214,7 @@ func (r *banSteamRepository) updateBan(ctx context.Context, ban *domain.BanSteam Set("target_id", ban.TargetID.Int64()). Set("appeal_state", ban.AppealState). Set("include_friends", ban.IncludeFriends). + Set("evade_ok", ban.EvadeOk). Where(sq.Eq{"ban_id": ban.BanID}) return r.db.DBErr(r.db.ExecUpdateBuilder(ctx, query)) @@ -224,7 +225,7 @@ func (r *banSteamRepository) ExpiredBans(ctx context.Context) ([]domain.BanSteam Builder(). Select("ban_id", "target_id", "source_id", "ban_type", "reason", "reason_text", "note", "valid_until", "origin", "created_on", "updated_on", "deleted", "case WHEN report_id is null THEN 0 ELSE report_id END", - "unban_reason_text", "is_enabled", "appeal_state", "include_friends"). + "unban_reason_text", "is_enabled", "appeal_state", "include_friends", "evade_ok"). From("ban"). Where(sq.And{sq.Lt{"valid_until": time.Now()}, sq.Eq{"deleted": false}}) @@ -246,7 +247,7 @@ func (r *banSteamRepository) ExpiredBans(ctx context.Context) ([]domain.BanSteam if errScan := rows.Scan(&ban.BanID, &targetID, &sourceID, &ban.BanType, &ban.Reason, &ban.ReasonText, &ban.Note, &ban.ValidUntil, &ban.Origin, &ban.CreatedOn, &ban.UpdatedOn, &ban.Deleted, &ban.ReportID, &ban.UnbanReasonText, - &ban.IsEnabled, &ban.AppealState, &ban.IncludeFriends); errScan != nil { + &ban.IsEnabled, &ban.AppealState, &ban.IncludeFriends, &ban.EvadeOk); errScan != nil { return nil, errors.Join(errScan, domain.ErrScanResult) } @@ -268,7 +269,7 @@ func (r *banSteamRepository) Get(ctx context.Context, filter domain.SteamBansQue builder := r.db. Builder(). Select("b.ban_id", "b.target_id", "b.source_id", "b.ban_type", "b.reason", - "b.reason_text", "b.note", "b.origin", "b.valid_until", "b.created_on", "b.updated_on", "b.include_friends", + "b.reason_text", "b.note", "b.origin", "b.valid_until", "b.created_on", "b.updated_on", "b.include_friends", "b.evade_ok", "b.deleted", "case WHEN b.report_id is null THEN 0 ELSE b.report_id END", "b.unban_reason_text", "b.is_enabled", "b.appeal_state", "s.personaname as source_personaname", "s.avatarhash", @@ -314,7 +315,7 @@ func (r *banSteamRepository) Get(ctx context.Context, filter domain.SteamBansQue builder = filter.QueryFilter.ApplySafeOrder(builder, map[string][]string{ "b.": { "ban_id", "target_id", "source_id", "ban_type", "reason", - "origin", "valid_until", "created_on", "updated_on", "include_friends", + "origin", "valid_until", "created_on", "updated_on", "include_friends", "evade_ok", "deleted", "report_id", "is_enabled", "appeal_state", }, "s.": {"source_personaname"}, @@ -355,7 +356,7 @@ func (r *banSteamRepository) Get(ctx context.Context, filter domain.SteamBansQue if errScan := rows. Scan(&person.BanID, &targetID, &sourceID, &person.BanType, &person.Reason, &person.ReasonText, &person.Note, &person.Origin, &person.ValidUntil, &person.CreatedOn, - &person.UpdatedOn, &person.IncludeFriends, &person.Deleted, &person.ReportID, &person.UnbanReasonText, + &person.UpdatedOn, &person.IncludeFriends, &person.EvadeOk, &person.Deleted, &person.ReportID, &person.UnbanReasonText, &person.IsEnabled, &person.AppealState, &person.SourceTarget.SourcePersonaname, &person.SourceTarget.SourceAvatarhash, &person.SourceTarget.TargetPersonaname, &person.SourceTarget.TargetAvatarhash, @@ -382,7 +383,7 @@ func (r *banSteamRepository) GetOlderThan(ctx context.Context, filter domain.Que Select("b.ban_id", "b.target_id", "b.source_id", "b.ban_type", "b.reason", "b.reason_text", "b.note", "b.origin", "b.valid_until", "b.created_on", "b.updated_on", "b.deleted", "case WHEN b.report_id is null THEN 0 ELSE s.report_id END", "b.unban_reason_text", "b.is_enabled", - "b.appeal_state", "b.include_friends"). + "b.appeal_state", "b.include_friends", "b.evade_ok"). From("ban b"). Where(sq.And{sq.Lt{"b.updated_on": since}, sq.Eq{"b.deleted": false}}) @@ -404,7 +405,7 @@ func (r *banSteamRepository) GetOlderThan(ctx context.Context, filter domain.Que if errQuery = rows.Scan(&ban.BanID, &targetID, &sourceID, &ban.BanType, &ban.Reason, &ban.ReasonText, &ban.Note, &ban.Origin, &ban.ValidUntil, &ban.CreatedOn, &ban.UpdatedOn, &ban.Deleted, &ban.ReportID, &ban.UnbanReasonText, - &ban.IsEnabled, &ban.AppealState, &ban.AppealState); errQuery != nil { + &ban.IsEnabled, &ban.AppealState, &ban.IncludeFriends, &ban.EvadeOk); errQuery != nil { return nil, errors.Join(errQuery, domain.ErrScanResult) } diff --git a/internal/ban/ban_steam_service.go b/internal/ban/ban_steam_service.go index 17a81b55..da8663f4 100644 --- a/internal/ban/ban_steam_service.go +++ b/internal/ban/ban_steam_service.go @@ -117,6 +117,7 @@ type apiBanRequest struct { DemoName string `json:"demo_name"` DemoTick int `json:"demo_tick"` IncludeFriends bool `json:"include_friends"` + EvadeOk bool `json:"evade_ok"` } func (h banHandler) onAPIPostBanSteamCreate() gin.HandlerFunc { @@ -160,7 +161,7 @@ func (h banHandler) onAPIPostBanSteamCreate() gin.HandlerFunc { var banSteam domain.BanSteam if errBanSteam := domain.NewBanSteam(author.SteamID, targetID, duration, req.Reason, req.ReasonText, req.Note, - origin, req.ReportID, req.BanType, req.IncludeFriends, &banSteam, + origin, req.ReportID, req.BanType, req.IncludeFriends, req.EvadeOk, &banSteam, ); errBanSteam != nil { httphelper.ResponseErr(ctx, http.StatusBadRequest, domain.ErrBadRequest) @@ -409,6 +410,7 @@ func (h banHandler) onAPIPostBanUpdate() gin.HandlerFunc { ReasonText string `json:"reason_text"` Note string `json:"note"` IncludeFriends bool `json:"include_friends"` + EvadeOk bool `json:"evade_ok"` ValidUntil time.Time `json:"valid_until"` } @@ -454,6 +456,7 @@ func (h banHandler) onAPIPostBanUpdate() gin.HandlerFunc { bannedPerson.BanType = req.BanType bannedPerson.Reason = req.Reason bannedPerson.IncludeFriends = req.IncludeFriends + bannedPerson.EvadeOk = req.EvadeOk bannedPerson.ValidUntil = req.ValidUntil if errSave := h.bu.Save(ctx, &bannedPerson.BanSteam); errSave != nil { diff --git a/internal/ban/ban_steam_usecase.go b/internal/ban/ban_steam_usecase.go index 71e985a0..f3c05db3 100644 --- a/internal/ban/ban_steam_usecase.go +++ b/internal/ban/ban_steam_usecase.go @@ -188,6 +188,14 @@ func (s *banSteamUsecase) IsOnIPWithBan(ctx context.Context, curUser domain.Pers return false, errMatch } + if existing.EvadeOk { + slog.Warn("Whitelisted player connecting from a banned ip", + slog.String("sid", existing.TargetID.String()), + slog.String("reason", existing.Reason.String())) + + return false, nil + } + duration, errDuration := util.ParseUserStringDuration("10y") if errDuration != nil { return false, errDuration @@ -205,7 +213,7 @@ func (s *banSteamUsecase) IsOnIPWithBan(ctx context.Context, curUser domain.Pers if errNewBan := domain.NewBanSteam(steamid.New(s.configUsecase.Config().General.Owner), steamID, duration, domain.Evading, domain.Evading.String(), "Connecting from same IP as banned player", domain.System, - 0, domain.Banned, false, &newBan); errNewBan != nil { + 0, domain.Banned, false, false, &newBan); errNewBan != nil { slog.Error("Could not create evade ban", log.ErrAttr(errDuration)) return false, errNewBan diff --git a/internal/chat/chat_usecase.go b/internal/chat/chat_usecase.go index b5922d90..148457e5 100644 --- a/internal/chat/chat_usecase.go +++ b/internal/chat/chat_usecase.go @@ -73,7 +73,7 @@ func (u chatUsecase) onWarningExceeded(ctx context.Context, newWarning domain.Ne if errNewBan := domain.NewBanSteam(u.owner, newWarning.UserMessage.SteamID, duration, newWarning.WarnReason, "", "Automatic warning ban", domain.System, 0, domain.NoComm, false, - &banSteam); errNewBan != nil { + false, &banSteam); errNewBan != nil { return errors.Join(errNewBan, domain.ErrFailedToBan) } } diff --git a/internal/database/migrations/000084_ban_whitelist.down.sql b/internal/database/migrations/000084_ban_whitelist.down.sql new file mode 100644 index 00000000..9ed5f1ad --- /dev/null +++ b/internal/database/migrations/000084_ban_whitelist.down.sql @@ -0,0 +1,6 @@ +BEGIN; + +alter table ban + drop column evade_ok; + +COMMIT; diff --git a/internal/database/migrations/000084_ban_whitelist.up.sql b/internal/database/migrations/000084_ban_whitelist.up.sql new file mode 100644 index 00000000..e71a5b57 --- /dev/null +++ b/internal/database/migrations/000084_ban_whitelist.up.sql @@ -0,0 +1,6 @@ +BEGIN; + +alter table ban + add column evade_ok bool not null default false; + +COMMIT; diff --git a/internal/discord/discord_service.go b/internal/discord/discord_service.go index 40f01fbf..590ea25c 100644 --- a/internal/discord/discord_service.go +++ b/internal/discord/discord_service.go @@ -790,7 +790,7 @@ func (h discordService) makeOnMute() func(context.Context, *discordgo.Session, * var banSteam domain.BanSteam if errOpts := domain.NewBanSteam(author.SteamID, playerID, duration, reason, reason.String(), modNote, - domain.Bot, 0, domain.NoComm, false, &banSteam); errOpts != nil { + domain.Bot, 0, domain.NoComm, false, false, &banSteam); errOpts != nil { return nil, errOpts } @@ -938,7 +938,7 @@ func (h discordService) onBanSteam(ctx context.Context, _ *discordgo.Session, var banSteam domain.BanSteam if errOpts := domain.NewBanSteam(author.SteamID, targetID, duration, reason, reason.String(), modNote, domain.Bot, - 0, domain.Banned, false, &banSteam); errOpts != nil { + 0, domain.Banned, false, false, &banSteam); errOpts != nil { return nil, errOpts } diff --git a/internal/domain/ban.go b/internal/domain/ban.go index 8d5403ff..14aa101b 100644 --- a/internal/domain/ban.go +++ b/internal/domain/ban.go @@ -308,6 +308,7 @@ type BanSteam struct { VacBans int `json:"vac_bans"` GameBans int `json:"game_bans"` LastIP net.IP `json:"last_ip"` + EvadeOk bool `json:"evade_ok"` } //goland:noinspection ALL @@ -362,7 +363,7 @@ func newBaseBanOpts(source steamid.SteamID, target steamid.SteamID, duration tim func NewBanSteam(source steamid.SteamID, target steamid.SteamID, duration time.Duration, reason Reason, reasonText string, modNote string, origin Origin, reportID int64, banType BanType, - includeFriends bool, banSteam *BanSteam, + includeFriends bool, evadeOk bool, banSteam *BanSteam, ) error { var opts BanSteamOpts @@ -380,6 +381,7 @@ func NewBanSteam(source steamid.SteamID, target steamid.SteamID, duration time.D banSteam.ReportID = opts.ReportID banSteam.BanID = opts.BanID banSteam.IncludeFriends = includeFriends + banSteam.EvadeOk = evadeOk return nil } diff --git a/internal/srcds/srcds_service.go b/internal/srcds/srcds_service.go index 585e01c5..0a05ce6f 100644 --- a/internal/srcds/srcds_service.go +++ b/internal/srcds/srcds_service.go @@ -251,7 +251,7 @@ func (s *srcdsHandler) onAPIPostBanSteamCreate() gin.HandlerFunc { var banSteam domain.BanSteam if errBanSteam := domain.NewBanSteam(sourceID, targetID, duration, req.Reason, req.ReasonText, req.Note, origin, - req.ReportID, req.BanType, req.IncludeFriends, &banSteam); errBanSteam != nil { + req.ReportID, req.BanType, req.IncludeFriends, false, &banSteam); errBanSteam != nil { httphelper.ResponseErr(ctx, http.StatusBadRequest, domain.ErrBadRequest) return