First version

This commit is contained in:
andy committed 2026-10-15 20:44:05 -03:00
commit 243e92a9dd
84 files changed
+4012

No files matched your search

+11
View File
@@ -0,0 +1,11 @@
[Unit]
Description=Contest firewall DNS filter (answers only the allowed names)
After=network.target firewall.service
PartOf=firewall.service
# Started and stopped with firewall.service (no [Install] section on purpose)
[Service]
Type=simple
ExecStart=/usr/sbin/dnsmasq --keep-in-foreground --conf-file=/run/contest-firewall/dnsmasq.conf
Restart=always
RestartSec=2
+50
View File
@@ -0,0 +1,50 @@
# Local proxy of the contest firewall (separate from the nginx of the image).
# The firewall redirects every outgoing connection on port 443 to this proxy,
# which reads the site name sent by the browser (TLS SNI), refuses names that
# are not in allowed_domains.txt and connects to the allowed sites itself.
# The proxy resolves the names on its own, so a program that forces the
# address of an allowed site for another name gets nowhere. Port 80 only
# answers with a redirect to HTTPS.
load_module /usr/lib/nginx/modules/ngx_stream_module.so;
user contest-proxy;
pid /run/contest-proxy.pid;
worker_processes 2;
worker_rlimit_nofile 8192;
error_log stderr warn;
events {
worker_connections 2048;
}
stream {
# resolver line (the local DNS filter), written by /firewall.sh
include /run/contest-firewall/proxy_resolver.conf;
map $ssl_preread_server_name $upstream {
hostnames;
# one "<name> <name>:443;" line per allowed domain, written by /firewall.sh
include /run/contest-firewall/proxy_names.conf;
default "";
}
log_format sni '$remote_addr "$ssl_preread_server_name" $status $upstream_addr';
access_log syslog:server=unix:/dev/log,tag=contest_proxy,nohostname sni;
server {
listen 127.0.0.1:8443;
ssl_preread on;
preread_timeout 10s;
proxy_connect_timeout 10s;
proxy_pass $upstream;
}
}
http {
access_log off;
server {
listen 127.0.0.1:8080;
return 301 https://$host$request_uri;
}
}
+14
View File
@@ -0,0 +1,14 @@
[Unit]
Description=Contest firewall proxy (checks the site name of HTTPS connections)
After=network.target firewall.service
PartOf=firewall.service
# Started and stopped with firewall.service (no [Install] section on purpose)
[Service]
Type=simple
ExecStartPre=/usr/sbin/nginx -t -q -c /etc/contest-firewall/contest-proxy.conf
ExecStart=/usr/sbin/nginx -c /etc/contest-firewall/contest-proxy.conf -g 'daemon off;'
ExecReload=/bin/kill -HUP $MAINPID
Restart=always
RestartSec=2
+296
View File
@@ -0,0 +1,296 @@
#!/usr/bin/python3
"""Install contest compiler defaults for the installed IDEs and clean team homes."""
import configparser
import io
import json
import os
from pathlib import Path
import pwd
import subprocess
import xml.etree.ElementTree as ET
changed = 0
JAVA_HOME = '/usr/lib/jvm/java-25-openjdk-amd64'
JAVA_OPTIONS = '-Dfile.encoding=UTF-8 -XX:+UseSerialGC -Xss64m -XX:TieredStopAtLevel=1 -XX:ActiveProcessorCount=2 -Xms2048m -Xmx2048m'
def write(path, text):
global changed
path = Path(path)
if not path.exists() or path.read_text() != text:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text)
changed += 1
def write_json(path, data):
write(path, json.dumps(data, indent=2) + '\n')
def read_json(path, default):
return json.loads(path.read_text()) if path.exists() else default
def geany(language, commands):
path = Path('/usr/share/geany/filedefs/filetypes.' + language)
conf = configparser.ConfigParser(interpolation=None, strict=False)
conf.optionxform = str
conf.read(path)
if not conf.has_section('build-menu'):
conf.add_section('build-menu')
for key, value in commands.items():
conf.set('build-menu', key, value)
stream = io.StringIO()
conf.write(stream, space_around_delimiters=False)
write(path, stream.getvalue())
for lang, cc in [('c', 'acpc-gcc'), ('cpp', 'acpc-g++')]:
geany(lang, {'FT_00_LB': '_Compile', 'FT_00_CM': cc + ' -c "%f" -o "%e.o"',
'FT_00_WD': '', 'FT_01_LB': '_Build', 'FT_01_CM': cc + ' "%f" -o "%e"',
'FT_01_WD': '', 'EX_00_LB': '_Execute', 'EX_00_CM': '"./%e"', 'EX_00_WD': ''})
geany('java', {'FT_00_LB': '_Compile', 'FT_00_CM': 'compilejava "%f"', 'FT_00_WD': '',
'FT_01_LB': '_Build', 'FT_01_CM': 'compilejava "%f"', 'FT_01_WD': '',
'EX_00_LB': '_Execute', 'EX_00_CM': 'runjava "%e"', 'EX_00_WD': ''})
geany('python', {'FT_00_LB': '_Compile', 'FT_00_CM': 'pypy3 -m py_compile "%f"', 'FT_00_WD': '',
'FT_01_LB': '_Build', 'FT_01_CM': 'pypy3 -m py_compile "%f"', 'FT_01_WD': '',
'EX_00_LB': '_Execute', 'EX_00_CM': 'pypy3 "%f"', 'EX_00_WD': ''})
def child(parent, tag):
node = parent.find(tag)
return node if node is not None else ET.SubElement(parent, tag)
def string(parent, tag, value):
child(child(parent, tag), 'str').text = value
def component(parent, name):
for node in parent.findall('component'):
if node.get('name') == name:
return node
return ET.SubElement(parent, 'component', name=name)
def option(parent, name, value):
node = next((n for n in parent.findall('option') if n.get('name') == name), None)
if node is None:
node = ET.SubElement(parent, 'option', name=name)
node.set('value', value)
def write_xml(path, root):
ET.indent(root)
write(path, ET.tostring(root, encoding='unicode') + '\n')
def preferences(path, values):
"""Update Java .properties keys, preserving unrelated workspace choices."""
data = {}
if path.exists():
for line in path.read_text().splitlines():
if '=' in line and not line.startswith('#'):
key, value = line.split('=', 1)
data[key] = value
for key, value in {'eclipse.preferences.version': '1', **values}.items():
data[key] = str(value).replace('\\', '\\\\').replace('\n', r'\n').replace('=', r'\=')
write(path, ''.join(k + '=' + v + '\n' for k, v in sorted(data.items())))
# Ask PyDev's own probe for the library paths of the installed PyPy.
probe = next(Path('/opt/eclipse/plugins').glob('org.python.pydev.core_*/pysrc/interpreterInfo.py'))
pyinfo = ET.fromstring(subprocess.check_output(['/usr/local/bin/pypy3', str(probe)], text=True))
ET.SubElement(pyinfo, 'name').text = 'ACPC PyPy 3.11'
ET.SubElement(pyinfo, 'vmArgs').text = ''
for lib in pyinfo.findall('lib'):
lib.attrib.clear()
pyinfo_text = ET.tostring(pyinfo, encoding='unicode') + '&&&&&'
for home in (Path('/home/team'), Path('/etc/skel')):
# Code::Blocks uses these settings for both single files and new projects.
cb_path = home / '.config/codeblocks/default.conf'
cb = ET.fromstring(cb_path.read_text()) if cb_path.exists() else ET.Element('CodeBlocksConfig', version='1')
compiler = child(cb, 'compiler')
string(compiler, 'SETTINGS_VERSION', '0.0.3')
string(compiler, 'DEFAULT_COMPILER', 'gcc')
gcc = child(child(compiler, 'sets'), 'gcc')
# Code::Blocks prepends MASTER_PATH/bin even to an absolute compiler name.
for key, value in {'NAME': 'ACPC GCC 15', 'MASTER_PATH': '/usr/local',
'EXTRA_PATHS': '/usr/bin',
'C_COMPILER': 'acpc-gcc', 'CPP_COMPILER': 'acpc-g++',
'LINKER': 'acpc-g++', 'MAKE': 'make', 'LIB_LINKER': 'ar'}.items():
string(gcc, key, value)
ET.indent(cb)
write(cb_path, '<?xml version="1.0" encoding="UTF-8"?>\n' + ET.tostring(cb, encoding='unicode') + '\n')
code = home / '.config/Code/User'
settings = read_json(code / 'settings.json', {})
settings.update({
'python.defaultInterpreterPath': '/usr/local/bin/pypy3',
'C_Cpp.default.compilerPath': '/usr/bin/g++-15',
'C_Cpp.default.cStandard': 'c23', 'C_Cpp.default.cppStandard': 'c++23',
'java.jdt.ls.java.home': JAVA_HOME,
'java.configuration.runtimes': [{'name': 'JavaSE-25', 'path': JAVA_HOME, 'default': True}],
'java.debug.settings.vmArgs': JAVA_OPTIONS,
'debug.onTaskErrors': 'abort',
})
write_json(code / 'settings.json', settings)
tasks = read_json(code / 'tasks.json', {'version': '2.0.0', 'tasks': []})
tasks['tasks'] = [t for t in tasks['tasks'] if not t.get('label', '').startswith('ACPC:')]
for action in ('build', 'run'):
task = {'label': 'ACPC: ' + ('Build' if action == 'build' else 'Build and run'),
'type': 'process', 'command': '/usr/local/bin/acpc-solution',
'args': [action, '${file}'], 'options': {'cwd': '${fileDirname}'},
'problemMatcher': ['$gcc'],
'presentation': {'reveal': 'always', 'focus': True, 'panel': 'shared', 'clear': True}}
if action == 'build': task['group'] = {'kind': 'build', 'isDefault': True}
tasks['tasks'].append(task)
write_json(code / 'tasks.json', tasks)
bindings = read_json(code / 'keybindings.json', [])
bindings = [b for b in bindings if not (b.get('key') == 'f6' or str(b.get('args', '')).startswith('ACPC:'))]
bindings.append({'key': 'f6', 'command': 'workbench.action.tasks.runTask',
'args': 'ACPC: Build and run', 'when': 'editorTextFocus'})
write_json(code / 'keybindings.json', bindings)
(home / 'contest').mkdir(exist_ok=True)
project_path = home / 'contest/.project'
if not project_path.exists():
write(project_path, '<projectDescription><name>contest</name><comment>ACPC solutions</comment><projects/><buildSpec/><natures/></projectDescription>\n')
preferences(home / 'contest/.settings/org.eclipse.core.resources.prefs', {'encoding/<project>': 'UTF-8'})
workspace = home / 'eclipse-workspace'
prefs = workspace / '.metadata/.plugins/org.eclipse.core.runtime/.settings'
preferences(prefs / 'org.python.pydev.prefs', {
'INTERPRETER_PATH_NEW': pyinfo_text,
# PyDev's first-editor prompt would disable automatic Java/CDT builds
# across this mixed-language workspace. Keep those defaults and skip it.
'CHECK_PREFERRED_PYDEV_SETTINGS': 'false',
})
preferences(prefs / 'org.eclipse.jdt.core.prefs', {
'org.eclipse.jdt.core.compiler.compliance': '25',
'org.eclipse.jdt.core.compiler.source': '25',
'org.eclipse.jdt.core.compiler.codegen.targetPlatform': '25',
})
vm_type = 'org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType'
vm_id = 'acpc-java-25'
vms = ET.Element('vmSettings', defaultVM=f'{len(vm_type)},{vm_type}{len(vm_id)},{vm_id}', defaultVMConnector='')
vmtype = ET.SubElement(vms, 'vmType', id=vm_type)
ET.SubElement(vmtype, 'vm', id=vm_id, name='ACPC Java 25', path=JAVA_HOME, vmArgs=JAVA_OPTIONS)
preferences(prefs / 'org.eclipse.jdt.launching.prefs', {'org.eclipse.jdt.launching.PREF_VM_XML': ET.tostring(vms, encoding='unicode')})
preferences(prefs / 'org.eclipse.ui.prefs', {'showIntro': 'false'})
preferences(prefs / 'org.eclipse.core.resources.prefs', {'encoding': 'UTF-8'})
for action in ('build', 'run'):
title = 'ACPC ' + ('Build' if action == 'build' else 'Build and run')
launch = ET.Element('launchConfiguration', type='org.eclipse.ui.externaltools.ProgramLaunchConfigurationType')
for key, value in {
'org.eclipse.ui.externaltools.ATTR_LOCATION': '/usr/local/bin/acpc-solution',
'org.eclipse.ui.externaltools.ATTR_TOOL_ARGUMENTS': action + ' "${resource_loc}"',
'org.eclipse.ui.externaltools.ATTR_WORKING_DIRECTORY': '${container_loc}',
'org.eclipse.debug.core.ATTR_REFRESH_SCOPE': '${workspace}',
}.items():
ET.SubElement(launch, 'stringAttribute', key=key, value=value)
for key, value in {
'org.eclipse.debug.ui.ATTR_LAUNCH_IN_BACKGROUND': 'true',
'org.eclipse.debug.core.ATTR_CAPTURE_OUTPUT': 'true',
'org.eclipse.debug.ui.ATTR_CONSOLE_OUTPUT_ON': 'true',
'org.eclipse.debug.ui.ATTR_CONSOLE_ERROR_ON': 'true',
}.items():
ET.SubElement(launch, 'booleanAttribute', key=key, value=value)
favorites = ET.SubElement(launch, 'listAttribute', key='org.eclipse.debug.ui.favoriteGroups')
ET.SubElement(favorites, 'listEntry', value='org.eclipse.ui.externaltools.launchGroup')
write_xml(workspace / '.metadata/.plugins/org.eclipse.debug.core/.launches' / (title + '.launch'), launch)
history_path = workspace / '.metadata/.plugins/org.eclipse.debug.ui/launchConfigurationHistory.xml'
history = ET.fromstring(history_path.read_text()) if history_path.exists() else ET.Element('launchHistory')
group_id = 'org.eclipse.ui.externaltools.launchGroup'
group = next((g for g in history if g.get('id') == group_id), None)
if group is None:
group = ET.SubElement(history, 'launchGroup', id=group_id)
child(group, 'mruHistory')
favorites = child(group, 'favorites')
for title in ('ACPC Build and run', 'ACPC Build'):
if not any(title == ET.fromstring(n.get('memento', '<x/>')).get('path') for n in favorites):
ET.SubElement(favorites, 'launch', memento=ET.tostring(ET.Element('launchConfiguration', local='true', path=title), encoding='unicode'))
write_xml(history_path, history)
for ide in ('clion', 'intellij-idea', 'pycharm'):
info = Path('/opt') / ide / 'product-info.json'
if not info.exists():
continue
options = home / '.config/JetBrains' / json.loads(info.read_text())['dataDirectoryName'] / 'options'
default_file = options / 'project.default.xml'
defaults = ET.fromstring(default_file.read_text()) if default_file.exists() else ET.Element('application')
project = child(component(defaults, 'ProjectManager'), 'defaultProject')
runs = component(project, 'RunManager')
if ide in ('pycharm', 'clion'):
sdk_file = options / 'jdk.table.xml'
sdks = ET.fromstring(sdk_file.read_text()) if sdk_file.exists() else ET.Element('application')
table = component(sdks, 'ProjectJdkTable')
sdk_name = 'PyPy 3.11 (as on Kattis)'
for node in list(table):
if node.find('name') is not None and node.find('name').get('value') == sdk_name:
table.remove(node)
sdk = ET.SubElement(table, 'jdk', version='2')
for key, value in [('name', sdk_name), ('type', 'Python SDK'),
('version', 'Python 3.11.15'), ('homePath', '/usr/local/bin/pypy3')]:
ET.SubElement(sdk, key, value=value)
roots = ET.SubElement(sdk, 'roots')
cp = ET.SubElement(ET.SubElement(roots, 'classPath'), 'root', type='composite')
for lib in pyinfo.findall('lib'):
ET.SubElement(cp, 'root', url='file://' + lib.text, type='simple')
ET.SubElement(ET.SubElement(roots, 'sourcePath'), 'root', type='composite')
additional = ET.SubElement(sdk, 'additional', SDK_UUID='3f7d6c1e-2a4b-4c8d-9e0f-a1b2c3d4e5f6')
ET.SubElement(additional, 'setting', name='FLAVOR_ID', value='PyPySdkFlavor')
ET.SubElement(additional, 'setting', name='FLAVOR_DATA', value='{}')
write_xml(sdk_file, sdks)
root_manager = component(project, 'ProjectRootManager')
root_manager.attrib.update({'version': '2', 'project-jdk-name': sdk_name, 'project-jdk-type': 'Python SDK'})
if ide == 'clion':
chain_file = options / 'linux/toolchains.xml'
chains = ET.fromstring(chain_file.read_text()) if chain_file.exists() else ET.Element('application')
cpp = component(chains, 'CPPToolchains')
cpp.set('version', '10')
entries = child(cpp, 'toolchains')
entries.set('detectedVersion', '5')
chain = entries.find('toolchain')
if chain is None:
chain = ET.SubElement(entries, 'toolchain', name='Default', toolSetKind='SYSTEM_UNIX_TOOLSET', debuggerKind='BUNDLED_GDB')
chain.set('customCCompilerPath', '/usr/local/bin/acpc-gcc')
chain.set('customCXXCompilerPath', '/usr/local/bin/acpc-g++')
write_xml(chain_file, chains)
for node in list(runs):
if node.get('default') == 'true' and node.get('type') == 'CppFileRunConfiguration':
runs.remove(node)
cfg = ET.SubElement(runs, 'configuration', default='true', type='CppFileRunConfiguration', factoryName='CppFileRunConfiguration')
option(cfg, 'compilerFile', '/usr/local/bin/acpc-cc')
option(cfg, 'compilerOptions', '')
if ide == 'intellij-idea':
sdk_file = options / 'jdk.table.xml'
sdks = ET.fromstring(sdk_file.read_text()) if sdk_file.exists() else ET.Element('application')
table = component(sdks, 'ProjectJdkTable')
for node in list(table):
if node.find('name') is not None and node.find('name').get('value') == 'ACPC Java 25':
table.remove(node)
sdk = ET.SubElement(table, 'jdk', version='2')
for key, value in [('name', 'ACPC Java 25'), ('type', 'JavaSDK'),
('version', 'java version "25.0.4.1"'), ('homePath', JAVA_HOME)]:
ET.SubElement(sdk, key, value=value)
roots = ET.SubElement(sdk, 'roots')
cp = ET.SubElement(ET.SubElement(roots, 'classPath'), 'root', type='composite')
for module in sorted(Path(JAVA_HOME, 'jmods').glob('*.jmod')):
ET.SubElement(cp, 'root', url=f'jrt://{JAVA_HOME}!/{module.stem}', type='simple')
ET.SubElement(ET.SubElement(roots, 'sourcePath'), 'root', type='composite')
ET.SubElement(sdk, 'additional')
write_xml(sdk_file, sdks)
root_manager = component(project, 'ProjectRootManager')
root_manager.attrib.update({'version': '2', 'languageLevel': 'JDK_25',
'project-jdk-name': 'ACPC Java 25', 'project-jdk-type': 'JavaSDK'})
for kind, factory in [('Application', 'Application'), ('JavaScratch', 'Java Scratch')]:
for node in list(runs):
if node.get('default') == 'true' and node.get('type') == kind:
runs.remove(node)
cfg = ET.SubElement(runs, 'configuration', default='true', type=kind, factoryName=factory)
option(cfg, 'VM_PARAMETERS', JAVA_OPTIONS)
option(cfg, 'ALTERNATIVE_JRE_PATH', JAVA_HOME)
option(cfg, 'ALTERNATIVE_JRE_PATH_ENABLED', 'true')
encoding = component(project, 'Encoding')
for node in list(encoding):
if node.get('url') == 'PROJECT':
encoding.remove(node)
ET.SubElement(encoding, 'file', url='PROJECT', charset='UTF-8')
write_xml(default_file, defaults)
if home == Path('/home/team'):
account = pwd.getpwnam('team')
for folder in (home / '.config/codeblocks', code, home / 'contest', home / '.config/JetBrains', workspace):
for path in [folder, *folder.rglob('*')]:
os.chown(path, account.pw_uid, account.pw_gid)
print(f'changed={changed}')
+46
View File
@@ -0,0 +1,46 @@
#!/bin/bash
# Usage: jetbrains_first_run.sh HOME_DIR
# Settings of the JetBrains IDEs (CLion, IntelliJ IDEA, PyCharm) for the team
# account, so that the teams go straight to work:
# - user agreement accepted, no usage data sharing (first-launch questions)
# - the home directory /home/team trusted (no "Trust project?" question)
# Compiler and interpreter defaults are installed by configure_ides.py.
# The folder name under .java is how Java stores the preference node
# "privacy_policy".
set -eu
dest_home=$1
prefs="$dest_home/.java/.userPrefs/jetbrains/"$'_!(!!cg"p!(}!}@"j!(k!|w"w!\'8!b!"p!\':!e@=='
consent="$dest_home/.local/share/JetBrains/consentOptions"
mkdir -p "$prefs" "$consent"
cat > "$prefs/prefs.xml" <<'XML'
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE map SYSTEM "http://java.sun.com/dtd/preferences.dtd">
<map MAP_XML_VERSION="1.0">
<entry key="eua_accepted_version" value="2.0"/>
</map>
XML
printf 'rsch.send.usage.stat:1.1:0:1791991599000' > "$consent/accepted"
for ide in pycharm intellij-idea clion; do
[ -f "/opt/$ide/product-info.json" ] || continue
dir=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["dataDirectoryName"])' "/opt/$ide/product-info.json")
options="$dest_home/.config/JetBrains/$dir/options"
mkdir -p "$options"
cat > "$options/trusted-paths.xml" <<'XML'
<application>
<component name="Trusted.Paths.Settings">
<option name="TRUSTED_PATHS">
<list>
<option value="/home/team" />
</list>
</option>
</component>
</application>
XML
done
if [ "$dest_home" != /etc/skel ]; then
owner=$(stat -c %U:%G "$dest_home")
chown -R "$owner" "$dest_home/.java" "$dest_home/.local" "$dest_home/.config/JetBrains"
fi
+19
View File
@@ -0,0 +1,19 @@
#!/bin/bash
# Turns the contest firewall off (all traffic allowed) for maintenance.
systemctl stop firewall.service
systemctl disable firewall.service
systemctl stop refresh_rules.service
systemctl stop refresh_rules.timer
systemctl disable refresh_rules.service
systemctl disable refresh_rules.timer
nft flush ruleset
nft delete table inet filter 2>/dev/null
echo "Firewall disabled. All traffic is now allowed."
# Reset DNS to its default behaviour (systemd-resolved instead of the filter)
ln -sfn ../run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
systemctl restart systemd-resolved
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
# Turns the contest firewall on and keeps it on across reboots.
systemctl enable firewall.service
systemctl start firewall.service
systemctl enable refresh_rules.service
systemctl start refresh_rules.service
systemctl enable refresh_rules.timer
systemctl start refresh_rules.timer
echo "Firewall enabled."
+13
View File
@@ -0,0 +1,13 @@
[Unit]
Description=Contest firewall
Wants=network-online.target contest-proxy.service contest-dns.service
After=network.target network-online.target
Before=getty.target getty@tty1.service
[Service]
Type=oneshot
ExecStart=/firewall.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
+90
View File
@@ -0,0 +1,90 @@
#!/bin/bash
# Base nftables ruleset: inbound traffic is dropped, outbound traffic is rejected
# except loopback, established connections, the local proxy (web), the local
# DNS filter and the time servers that /refresh_rules.sh adds.
# Web traffic (ports 80 and 443) of every program is redirected to the local
# proxy (contest-proxy.service), which checks the site name. Only the proxy
# may connect to the outside on port 443.
CONF=/etc/contest-firewall
# Remove any persisting nftables rules
rm -f /etc/nftables.rules
touch /etc/nftables.rules
# Configuration of the proxy: the allowed names and the DNS resolver
mkdir -p /run/contest-firewall
for name in $(sed -e 's/#.*//' -e 's/[[:space:]]//g' "$CONF/allowed_domains.txt"); do
echo "$name $name:443;"
done > /run/contest-firewall/proxy_names.conf
# The proxy resolves the names through the DNS filter below
echo "resolver 127.0.0.1 ipv6=off valid=30s;" > /run/contest-firewall/proxy_resolver.conf
# Local DNS filter (contest-dns.service): the allowed domains and the time
# servers are forwarded to the DNS resolvers, every other name gets no answer.
# Programs on the machine ask it through /etc/resolv.conf, and only the proxy
# account (which also runs the filter) may reach the DNS resolvers. This
# closes DNS as a way to exchange data with a server outside.
{
echo "listen-address=127.0.0.1"
echo "bind-interfaces"
echo "no-resolv"
echo "no-hosts"
echo "user=contest-proxy"
echo "group=nogroup"
echo "cache-size=1000"
echo "log-queries"
echo "server=/#/"
for name in $(sed -e 's/#.*//' -e 's/[[:space:]]//g' "$CONF/allowed_domains.txt" "$CONF/ntp_servers.txt"); do
for dns in $(sed -e 's/#.*//' -e 's/[[:space:]]//g' "$CONF/dns_servers.txt"); do
echo "server=/$name/$dns"
done
done
} > /run/contest-firewall/dnsmasq.conf
# /run/resolvconf is one of the places the AppArmor profiles (dig, ...) accept,
# and the resolvconf package that would also use it is not installed
mkdir -p /run/resolvconf
echo "nameserver 127.0.0.1" > /run/resolvconf/resolv.conf
ln -sfn /run/resolvconf/resolv.conf /etc/resolv.conf
# Flush all rules, then create the table, chains and address sets
nft flush ruleset
nft add table inet filter
nft add chain inet filter input '{ type filter hook input priority 0 ; policy drop ; }'
nft add chain inet filter forward '{ type filter hook forward priority 0 ; policy drop ; }'
nft add chain inet filter output '{ type filter hook output priority 0 ; policy accept ; }'
nft add set inet filter ntp_ips '{ type ipv4_addr ; flags interval ; }'
# Redirect web traffic to the proxy, except the proxy's own connections and
# the local sites of the image (DevDocs)
nft add table inet nat
nft add chain inet nat output '{ type nat hook output priority -100 ; }'
nft add rule inet nat output meta skuid contest-proxy return
nft add rule inet nat output ip daddr 127.0.0.0/8 return
nft add rule inet nat output meta nfproto ipv4 tcp dport 443 redirect to :8443
nft add rule inet nat output meta nfproto ipv4 tcp dport 80 redirect to :8080
# Established/related connections and loopback
nft add rule inet filter input ct state related,established accept
nft add rule inet filter output ct state related,established accept
nft add rule inet filter input iif lo accept
nft add rule inet filter output oif lo accept
# Connections redirected to the proxy (they still show the network interface here)
nft add rule inet filter output ip daddr 127.0.0.1 tcp dport '{ 8080, 8443 }' ct status dnat accept
# Web: only the proxy connects to the outside, by name, to the allowed sites
# it resolves itself through the DNS filter (no list of addresses to keep up
# to date, content delivery networks change them all the time)
nft add rule inet filter output meta skuid contest-proxy tcp dport 443 accept
# Time servers, for chrony only (addresses filled by /refresh_rules.sh)
nft add rule inet filter output meta skuid _chrony ip daddr @ntp_ips udp dport 123 accept
# DNS resolvers, for the proxy and the DNS filter only
for dns in $(sed -e 's/#.*//' -e 's/[[:space:]]//g' "$CONF/dns_servers.txt"); do
nft add rule inet filter output meta skuid contest-proxy ip daddr "$dns" udp dport 53 accept
nft add rule inet filter output meta skuid contest-proxy ip daddr "$dns" tcp dport 53 accept
done
# Reject any other outgoing connection
nft add rule inet filter output reject
+104
View File
@@ -0,0 +1,104 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; base-uri 'none'; form-action 'none'">
<title>Contest Help (offline) · ACPC 2026</title>
<style>
:root{color-scheme:light;--ink:#1c3043;--muted:#4a6275;--line:#d4e0e6;--accent:#14615c}
*{box-sizing:border-box}body{margin:0;background:#f3f6f8;color:var(--ink);font:17px/1.6 system-ui,sans-serif}
header{background:#153746;color:#fff;padding:36px max(24px,calc((100vw - 1180px)/2))}
header p{max-width:800px;color:#d4e8ed;margin:8px 0}h1{font-size:clamp(28px,4vw,42px);line-height:1.2;margin:8px 0 16px}
.eyebrow{font-size:14px;letter-spacing:.08em;text-transform:uppercase}a{color:#115f70;text-underline-offset:3px}a:focus-visible,button:focus-visible,input:focus-visible{outline:3px solid #c08619;outline-offset:3px}
.layout{max-width:1228px;margin:0 auto;display:grid;grid-template-columns:250px minmax(0,1fr);gap:32px;padding:32px 24px}
aside{align-self:start;position:sticky;top:24px}label{font-weight:650;display:block;margin-bottom:6px}input{font:inherit;padding:10px 12px;border:1px solid #8096a4;border-radius:6px;width:100%}button{font:inherit;padding:5px 12px;cursor:pointer;background:#fff;border:1px solid #8096a4;border-radius:5px;margin-top:8px}
nav{margin-top:24px}nav a{display:block;padding:5px 0;text-decoration:none}nav a:hover{text-decoration:underline}.count{color:var(--muted);font-size:14px;margin:8px 0}
article{background:#fff;padding:24px 28px;border:1px solid var(--line);border-radius:8px;margin:0 0 20px;scroll-margin-top:20px}article[hidden]{display:none}h2{font-size:24px;line-height:1.3;margin:0 0 14px}h3{font-size:18px;margin:18px 0 6px}p{margin:10px 0}li{margin:6px 0}ul,ol{padding-left:22px}code,pre{font-family:ui-monospace,monospace;font-size:.91em}code{background:#eef3f5;padding:1px 4px;border-radius:3px}pre{overflow-x:auto;background:#eef3f5;padding:16px;border-radius:5px;line-height:1.5}pre code{padding:0}
table{border-collapse:collapse;width:100%;font-size:16px}th,td{text-align:left;vertical-align:top;padding:10px 8px;border-bottom:1px solid var(--line)}th:first-child,td:first-child{width:160px}.sources{font-size:14px;color:var(--muted);border-top:1px solid var(--line);padding-top:12px;margin-top:20px}.sources a{margin-right:10px}footer{max-width:1180px;margin:0 auto;padding:0 24px 32px;color:var(--muted);font-size:14px}.empty{padding:30px;background:#fff;border:1px solid var(--line)}
@media(max-width:760px){.layout{display:block;padding:20px 16px}aside{position:static;margin-bottom:24px}nav{display:flex;flex-wrap:wrap;gap:4px 18px}article{padding:20px}header{padding:24px}th:first-child,td:first-child{width:120px}}
@media print{body{background:#fff;font-size:11pt}header{background:#fff;color:#000;padding:0}header p{color:#000}.layout{display:block;padding:0}aside,button{display:none}article,article[hidden]{display:block;border:0;padding:0;break-inside:avoid}a{color:inherit}.sources{font-size:9pt}footer{padding:0}}
</style>
</head>
<body>
<header><div class="eyebrow">ACPC 2026 · Local reference</div><h1>Contest Help (offline)</h1><p>Input, judging and language questions for C, C++, Java and Python 3. All articles on this page work without Internet access.</p></header>
<div class="layout">
<aside aria-label="Find an article">
<label for="search">Search help</label><input id="search" type="search" placeholder="Try EOF, Java or memory" autocomplete="off"><button id="clear" type="button">Clear search</button><p id="count" class="count" role="status" aria-live="polite">12 articles</p>
<nav aria-label="Articles">
<a href="#input-output">Input and output</a><a href="#eof">Reading until EOF</a><a href="#valid-input">Valid input</a><a href="#local-tests">Testing locally</a><a href="#judging">How judging works</a><a href="#judgements">Judgements</a><a href="#rejected">Investigating a rejection</a><a href="#c-cpp">C and C++</a><a href="#java-main">Java main class</a><a href="#java-io">Java input and formatting</a><a href="#java-errors">Java exceptions</a><a href="#python">Python 3 and modules</a>
</nav>
<p class="count"><a href="../../icpc/TechNotes.pdf">Technical Notes</a><br><a href="https://devdocs/">Language Documentation</a></p>
</aside>
<main id="articles">
<p id="empty" class="empty" hidden>No matching article. Try a shorter search or clear the search.</p>
<article id="input-output"><h2>Input and output</h2>
<p>Read the problem's input from standard input and write the answer to standard output. Follow the format in the statement. Do not print prompts, progress messages or debugging text as part of the answer.</p>
<p>For local tests, save the input in a file and redirect it into your program. Redirection supplies the file contents through standard input, just as typing would. You do not need to open a named input file in the program.</p>
<p class="sources">Source (online): <a href="https://support.kattis.com/support/solutions/articles/79000120852-how-do-i-handle-input-and-output-">Kattis input and output</a></p></article>
<article id="eof"><h2>Reading until EOF</h2>
<p>Some problems provide a test count or a terminating value. Others end when no input remains. Follow the rule in the statement.</p>
<p>End of file, or EOF, is a condition reported by a read operation. It is not a word to enter in the input. A redirected input file ends automatically. In a terminal, finish your input line with Enter, then press Ctrl+D on an empty line to signal EOF.</p>
<p class="sources">Source (online): <a href="https://support.kattis.com/support/solutions/articles/79000120853-how-do-i-read-input-if-the-end-is-not-defined-what-is-end-of-file-">Kattis end of file</a></p></article>
<article id="valid-input"><h2>Valid input</h2>
<p>The judge's input follows the problem's input specification. Your solution can rely on the stated ranges and structure. Include the boundary values that the statement allows when constructing local tests.</p>
<p class="sources">Source (online): <a href="https://support.kattis.com/support/solutions/articles/79000121166-how-do-i-handle-input-that-does-not-follow-the-input-specification-">Kattis input specification</a></p></article>
<article id="local-tests"><h2>Testing locally</h2>
<p>Use the sample input and expected output from the problem page, then test additional cases. Passing the samples does not prove the solution is correct for every permitted input.</p>
<p>For a C++ file named <code>main.cpp</code> and a sample saved as <code>sample.in</code>, the contest machine provides these commands:</p>
<pre><code>compilecpp main.cpp
runcpp &lt; sample.in &gt; actual.out
diff -u sample.ans actual.out</code></pre>
<p>Save the expected output in <code>sample.ans</code>. Read the <a href="../../icpc/TechNotes.pdf">Technical Notes</a> for the equivalent commands in the other languages and for IDE build actions. Local runs do not enforce the judge's time and memory limits. A text comparison can help locate differences, but the problem's output rules determine whether an answer is valid.</p>
<p class="sources">Source (online): <a href="https://support.kattis.com/support/solutions/articles/79000122495-can-i-test-my-solution-before-i-submit-">Kattis local testing</a>. Commands are specific to the ACPC image.</p></article>
<article id="judging"><h2>How judging works</h2>
<p>Kattis first compiles the submission or checks it as required by the chosen language. It then runs the program on test inputs and checks the answers against the problem's rules. Each test run starts a new process, so program variables do not carry over between test files.</p>
<p>The judge enforces resource limits and may stop checking after a failure. A rejection therefore does not describe every possible problem with the submission. Some problems allow several valid answers or a specified numerical tolerance. Read their output requirements carefully.</p>
<p class="sources">Source (online): <a href="https://support.kattis.com/support/solutions/articles/79000141262-how-judging-works">Kattis judging process</a></p></article>
<article id="judgements"><h2>Judgements</h2>
<table><thead><tr><th scope="col">Result</th><th scope="col">Meaning</th></tr></thead><tbody>
<tr><th scope="row">Accepted</th><td>The submission passed the required checks.</td></tr>
<tr><th scope="row">Compile Error</th><td>The build or language check failed. Read the compiler output.</td></tr>
<tr><th scope="row">Run Time Error</th><td>The program crashed, raised an uncaught exception or ended with a nonzero exit code.</td></tr>
<tr><th scope="row">Time Limit Exceeded</th><td>A run exceeded the time allowed for a test.</td></tr>
<tr><th scope="row">Wrong Answer</th><td>An answer did not satisfy the output requirements.</td></tr>
<tr><th scope="row">Output Limit Exceeded</th><td>The program produced too much output, possibly because of a loop or debugging prints.</td></tr>
<tr><th scope="row">Memory Limit Exceeded</th><td>The run needed more memory than allowed. Memory exhaustion can also appear as a run time error.</td></tr>
<tr><th scope="row">Judge Error</th><td>The judging system encountered a problem. Ask contest staff.</td></tr>
</tbody></table>
<p class="sources">Source (online): <a href="https://support.kattis.com/support/solutions/articles/79000141261-list-of-possible-judgements">Kattis judgement list</a></p></article>
<article id="rejected"><h2>Investigating a rejection</h2>
<p>Read the judgement and any compiler or run feedback available on the submission page. Check the selected language and submitted files, then reproduce the issue locally where possible.</p>
<p>Recheck input parsing, output formatting and the limits in the statement. Test small cases that you can solve by hand, boundary values and cases with different input structures. Remove debugging output before submitting again. Hidden test inputs are generally unavailable, so the samples alone are not a complete test suite.</p>
<p class="sources">Sources (online): <a href="https://support.kattis.com/support/solutions/articles/79000030851-my-submissions-are-getting-rejected-what-can-i-do-">Rejected submissions</a><a href="https://support.kattis.com/support/solutions/articles/79000138570-why-don-t-you-provide-the-secret-test-data-">Hidden test data</a></p></article>
<article id="c-cpp"><h2>C and C++</h2>
<p>Use an <code>int main(...)</code> entry point and finish successful execution with a zero exit status. A nonzero exit status is a run time error even if the printed answer looks correct.</p>
<p>For C++ programs with substantial input or output, stream synchronization and repeated flushing can be expensive. Before any stream I/O, <code>std::ios::sync_with_stdio(false);</code> and <code>std::cin.tie(nullptr);</code> can reduce that overhead. After disabling synchronization, keep input and output within the C++ stream API. Prefer <code>'\n'</code> to <code>std::endl</code> when a flush is unnecessary.</p>
<p class="sources">Sources (online): <a href="https://support.kattis.com/support/solutions/articles/79000122476-what-happens-if-main-does-not-return-0-">C exit status</a><a href="https://support.kattis.com/support/solutions/articles/79000122478-what-happens-if-main-does-not-return-0-">C++ exit status</a><a href="https://support.kattis.com/support/solutions/articles/79000122479-i-m-having-trouble-with-i-o-performance-with-cin-and-cout-in-c-what-can-i-do-about-it-">C++ I/O performance</a></p></article>
<article id="java-main"><h2>Java main class</h2>
<p>The main class is the class containing <code>public static void main(String[] args)</code>. Enter its class name, without <code>.java</code> or <code>.class</code>, when Kattis asks for it. If the class belongs to a package, the main class name includes that package.</p>
<p>For the local single-file workflow in the Technical Notes, use a file such as <code>Main.java</code>, a class named <code>Main</code> and no package declaration. Compile with <code>compilejava Main.java</code> and run with <code>runjava Main</code>.</p>
<p class="sources">Source (online): <a href="https://support.kattis.com/support/solutions/articles/79000122481-what-is-the-mainclass-for-java-why-do-i-get-unknown-mainclass-">Kattis Java main class</a>. Local commands are specific to the ACPC image.</p></article>
<article id="java-io"><h2>Java input and formatting</h2>
<p><code>Scanner</code> is convenient but may be too slow for large inputs. Buffered reading and writing can reduce I/O overhead. Read the input specification before choosing how to split lines or tokens.</p>
<p>Locale affects formatted numbers. Kattis documents the <code>en_US</code> locale for Java, which uses a decimal point. For predictable local formatting, specify <code>java.util.Locale.US</code> where the formatting API accepts a locale, and follow the problem's requested precision.</p>
<p class="sources">Sources (online): <a href="https://support.kattis.com/support/solutions/articles/79000122482-i-m-having-trouble-with-i-o-performance-what-can-i-do-about-it-">Java I/O performance</a><a href="https://support.kattis.com/support/solutions/articles/79000122483-what-locale-is-used-useful-for-e-g-printf-in-java-">Java locale</a></p></article>
<article id="java-errors"><h2>Java exceptions</h2>
<p>An uncaught exception normally causes a run time error. Memory exhaustion can be reported as a memory limit error. The feedback available from Kattis may be shorter than the stack trace you see locally.</p>
<p>When reproducing a failure, inspect the local exception and the line where it arose. Check bounds, parsing, null references and memory use against the problem's valid inputs.</p>
<p class="sources">Source (online): <a href="https://support.kattis.com/support/solutions/articles/79000122480-what-happens-with-java-exceptions-">Kattis Java exceptions</a></p></article>
<article id="python"><h2>Python 3 and modules</h2>
<p>The ACPC Python 3 runtime is PyPy 7.3.23, implementing Python 3.11.15. Use the configured PyPy interpreter or the <code>compilepython3</code> and <code>runpython3</code> commands for local tests. CPython and PyPy can differ in performance and some implementation details.</p>
<p>Use the Python standard library available in the contest runtime. Do not assume that a third-party module installed elsewhere will be available on the judge. An import of an unavailable module fails at run time. The local Python 3.11 documentation is available in <a href="https://devdocs/">Language Documentation</a>.</p>
<p class="sources">Sources (online): <a href="https://support.kattis.com/support/solutions/articles/79000122490-what-implementation-of-python-are-you-using-">Python implementation</a><a href="https://support.kattis.com/support/solutions/articles/79000122491-what-python-modules-are-available-">Python modules</a>. Version numbers are from the ACPC configuration.</p></article>
</main></div>
<footer>Prepared for ACPC 2026 from the linked Kattis support articles, checked on 15 October 2026. These are concise summaries with local instructions. The contest rules and problem statements take precedence. Source links require Internet access outside the contest restrictions.</footer>
<script>
const search=document.getElementById('search');
const articles=[...document.querySelectorAll('article')];
function filter(){const words=search.value.toLowerCase().trim().split(/\s+/).filter(Boolean);let count=0;for(const a of articles){a.hidden=!words.every(w=>a.textContent.toLowerCase().includes(w));if(!a.hidden)count++}document.getElementById('count').textContent=count+' of '+articles.length+' articles';document.getElementById('empty').hidden=count!==0}
search.addEventListener('input',filter);
document.getElementById('clear').addEventListener('click',()=>{search.value='';filter();search.focus()});
document.querySelectorAll('nav a').forEach(a=>a.addEventListener('click',()=>{search.value='';filter()}));
</script>
</body></html>
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/python3
"""GCC 15 driver used by IDE toolchains, with the contest language and link options."""
import json
import os
from pathlib import Path
import re
import shlex
import sys
def expand_response_files(arguments, depth=0):
# CLion also puts its language/standard probes in GCC response files.
# Inspect their contents before choosing C vs C++ or replacing flags.
if depth > 16:
raise SystemExit('ACPC: response files are nested too deeply')
expanded = []
for arg in arguments:
if arg.startswith('@') and Path(arg[1:]).is_file():
expanded.extend(expand_response_files(shlex.split(Path(arg[1:]).read_text()), depth + 1))
else:
expanded.append(arg)
return expanded
args = expand_response_files(sys.argv[1:])
name = Path(sys.argv[0]).name
sources = [a for a in args if Path(a).suffix in ('.c', '.C', '.cc', '.cpp', '.cxx', '.c++')]
explicit_cxx = any(a.startswith('-xc++') or (a == '-x' and i + 1 < len(args) and args[i + 1].startswith('c++'))
for i, a in enumerate(args))
cxx = name.endswith(('g++', 'c++')) or explicit_cxx or (name not in ('acpc-gcc', 'gcc', 'cc') and any(not a.endswith('.c') for a in sources))
compiler = '/usr/bin/g++-15' if cxx else '/usr/bin/gcc-15'
query = any(a in ('--version', '-dumpversion', '-dumpfullversion', '-dumpmachine', '-print-search-dirs')
or a.startswith(('-print-file-name=', '-print-prog-name=')) for a in args)
if not query:
args = [a for a in args if not a.startswith('-std=') and not re.fullmatch(r'-O(?:[0-3sgz]|fast)?', a)]
args = ['-g', '-O2', '-std=gnu++23' if cxx else '-std=gnu23'] + args
if not any(a in args for a in ('-c', '-S', '-E', '-M', '-MM', '-fsyntax-only', '-shared')):
args += ['-static']
args += ['-lrt', '-Wl,--whole-archive', '-lpthread', '-Wl,--no-whole-archive'] if cxx else ['-lm']
cmd = [compiler] + args
if not query and sources:
print('ACPC: ' + shlex.join(cmd), file=sys.stderr, flush=True)
if log := os.environ.get('ACPC_COMPILER_LOG'):
with open(log, 'a') as f:
f.write(json.dumps({'cwd': os.getcwd(), 'argv': cmd}) + '\n')
os.execv(compiler, cmd)
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
# Initialize the supplied contest workspace once, also after a team-home reset.
set -eu
export PATH="/usr/local/lib/acpc-toolchain:$PATH"
workspace="$HOME/eclipse-workspace"
mkdir -p "$workspace"
if [ ! -f "$workspace/.acpc-imported" ]; then
/opt/eclipse/eclipse -nosplash -data "$workspace" \
-application org.eclipse.cdt.managedbuilder.core.headlessbuild \
-import "$HOME/contest" -no-indexer > "$workspace/import.log" 2>&1
touch "$workspace/.acpc-imported"
fi
exec /opt/eclipse/eclipse -data "$workspace" "$@"
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/python3
"""Build or build-and-run the active contest source from an IDE task."""
import os
from pathlib import Path
import resource
import shlex
import subprocess
import sys
if len(sys.argv) != 3 or sys.argv[1] not in ('build', 'run'):
raise SystemExit('Usage: acpc-solution build|run SOURCE')
action, arg = sys.argv[1:]
src = Path(arg).resolve(strict=True)
os.chdir(src.parent)
ext = src.suffix
if ext == '.c':
build = ['/usr/local/bin/acpc-gcc', src.name, '-o', src.stem]
run = [str(src.with_suffix(''))]
elif ext in ('.cpp', '.cc', '.C', '.cxx', '.c++'):
build = ['/usr/local/bin/acpc-g++', src.name, '-o', src.stem]
run = [str(src.with_suffix(''))]
elif ext == '.java':
build = ['/usr/lib/jvm/java-25-openjdk-amd64/bin/javac', '-encoding', 'UTF-8',
'-sourcepath', '.', '-cp', './*', '-d', '.', src.name]
run = ['/usr/local/bin/runjava', src.stem]
elif ext in ('.py', '.py3'):
build = ['/usr/local/bin/pypy3', '-m', 'py_compile', src.name]
run = ['/usr/local/bin/pypy3', src.name]
else:
raise SystemExit('Use a C, C++, Java or Python source file.')
print('Build: ' + shlex.join(build), flush=True)
result = subprocess.run(build)
if result.returncode:
raise SystemExit(result.returncode)
if action == 'run':
print('Run: ' + shlex.join(run), flush=True)
soft, hard = resource.getrlimit(resource.RLIMIT_STACK)
resource.setrlimit(resource.RLIMIT_STACK, (hard, hard))
os.execv(run[0], run)
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
# Compiles a C program as Kattis does for ACPC 2026 (gcc-15 15.2.0). Result: a.out
exec gcc-15 -g -O2 -o a.out -std=gnu23 -static "$@" -lm
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
# Compiles a C++ program as Kattis does for ACPC 2026 (g++-15 15.2.0). Result: a.out
exec g++-15 -g -O2 -o a.out -std=gnu++23 -static -lrt -Wl,--whole-archive -lpthread -Wl,--no-whole-archive "$@"
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
# Compiles a Java program as Kattis does for ACPC 2026 (javac 25.0.4.1)
exec javac -encoding UTF-8 -sourcepath . -cp './*' -d . "$@"
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
# Checks the syntax of a Python 3 program with PyPy (Kattis runs it with pypy3 7.3.23)
exec pypy3 -m py_compile "$@"
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
# Runs the program compiled by compilec (a.out)
# Large stack (2 GB), for deep recursion
ulimit -s 2097152
exec ./a.out "$@"
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
# Runs the program compiled by compilecpp (a.out)
# Large stack (2 GB), for deep recursion
ulimit -s 2097152
exec ./a.out "$@"
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
# Runs a Java program as Kattis does for ACPC 2026 (java 25.0.4.1).
# Usage: runjava MainClass. Kattis sets the memory to the limit of the problem,
# 2048 MB here by default (MEMLIM=1024 runjava MainClass for another limit).
MEMLIM=${MEMLIM:-2048}
exec java -Dfile.encoding=UTF-8 -XX:+UseSerialGC -Xss64m -XX:TieredStopAtLevel=1 -XX:ActiveProcessorCount=2 -Xms${MEMLIM}m -Xmx${MEMLIM}m -cp '.:./*' "$@"
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
# Runs a Python 3 program as Kattis does for ACPC 2026 (pypy3 7.3.23, Python 3.11.15)
exec pypy3 "$@"
@@ -0,0 +1,9 @@
// ACPC 2026: the teams may not change the network (Wi-Fi, phone hotspot,
// modem, VPN, ...). Only the administrators (sudo group) keep these actions.
polkit.addRule(function(action, subject) {
if ((action.id.indexOf("org.freedesktop.NetworkManager.") == 0 ||
action.id.indexOf("org.freedesktop.ModemManager1.") == 0) &&
!subject.isInGroup("sudo")) {
return polkit.Result.NO;
}
});
@@ -0,0 +1,14 @@
// ACPC 2026: the teams may not restart, power off, suspend or hibernate the
// machine. A restart would let USBGuard accept a device plugged in before.
// Only the administrators (sudo group) keep these actions.
polkit.addRule(function(action, subject) {
if ((action.id.indexOf("org.freedesktop.login1.reboot") == 0 ||
action.id.indexOf("org.freedesktop.login1.power-off") == 0 ||
action.id.indexOf("org.freedesktop.login1.halt") == 0 ||
action.id.indexOf("org.freedesktop.login1.suspend") == 0 ||
action.id.indexOf("org.freedesktop.login1.hibernate") == 0 ||
action.id.indexOf("org.freedesktop.login1.set-reboot") == 0) &&
!subject.isInGroup("sudo")) {
return polkit.Result.NO;
}
});
@@ -0,0 +1,8 @@
// ACPC 2026: the teams may not mount USB sticks, disks or disk images.
// Only the administrators (sudo group) keep the udisks actions.
polkit.addRule(function(action, subject) {
if (action.id.indexOf("org.freedesktop.udisks2.") == 0 &&
!subject.isInGroup("sudo")) {
return polkit.Result.NO;
}
});
@@ -0,0 +1,7 @@
# ACPC 2026: the desktop user gets no direct access to phones and cameras
# (MTP and PTP, used to copy files), which bypasses the mounting of disks.
# The tags are set by 69-libmtp.rules and 60-libgphoto2, and used by
# 73-seat-late.rules, so this file sits between them.
ENV{ID_MTP_DEVICE}=="1", TAG-="uaccess"
ENV{ID_GPHOTO2}=="1", TAG-="uaccess"
ENV{ID_MEDIA_PLAYER}=="1", TAG-="uaccess"
+8
View File
@@ -0,0 +1,8 @@
# ACPC 2026: the power, reboot and suspend keys do nothing. A long press on
# the power button still cuts the power (hardware), in view of the proctors.
[Login]
HandlePowerKey=ignore
HandleRebootKey=ignore
HandleSuspendKey=ignore
HandleHibernateKey=ignore
HandleLidSwitch=ignore
+2
View File
@@ -0,0 +1,2 @@
# ACPC 2026: no SysRq key combinations (they can restart the machine)
kernel.sysrq = 0
+133
View File
@@ -0,0 +1,133 @@
#!/bin/bash
# Turns the contest restrictions off and on, for maintenance. Root only, so
# it is used through sudo from the administrator account (icpc).
#
# sudo contest-restrictions status
# sudo contest-restrictions report [SINCE] (refused attempts, default: today)
# sudo contest-restrictions off [all|firewall|usb|network]
# sudo contest-restrictions on [all|firewall|usb|network]
#
# firewall firewall, web proxy, DNS filter (/disable_firewall.sh, /enable_firewall.sh)
# usb USBGuard, USB disks and phones for the team account
# network network settings (Wi-Fi, hotspot, ...) for the team account
#
# "off" stays in effect after a reboot, until "on".
set -u
DISABLED=/etc/contest-restrictions/disabled
USB_FILES="/etc/polkit-1/rules.d/10-acpc-no-removable-media.rules /etc/udev/rules.d/72-acpc-no-phone-access.rules"
NETWORK_FILES="/etc/polkit-1/rules.d/10-acpc-no-network-changes.rules"
usage() {
sed -n '2,14p' "$0" | sed 's/^# \{0,1\}//'
exit 2
}
[ "$(id -u)" -eq 0 ] || { echo "Run it with sudo, from the administrator account." >&2; exit 1; }
mkdir -p "$DISABLED"
chmod 700 /etc/contest-restrictions
# Moves rule files out of the way (off) or back (on)
move_rules() {
action=$1
shift
for f in "$@"; do
if [ "$action" = off ] && [ -f "$f" ]; then
mv "$f" "$DISABLED/"
elif [ "$action" = on ] && [ -f "$DISABLED/$(basename "$f")" ]; then
mv "$DISABLED/$(basename "$f")" "$f"
fi
done
}
firewall() {
if [ "$1" = off ]; then
/disable_firewall.sh
else
/enable_firewall.sh
/refresh_rules.sh
fi
}
usb() {
if [ "$1" = off ]; then
systemctl disable --now usbguard.service
# USBGuard leaves the controllers refusing new devices when it stops
for f in /sys/bus/usb/devices/usb*/authorized_default; do
[ -e "$f" ] && echo 1 > "$f"
done
move_rules off $USB_FILES
udevadm control --reload
echo "USB devices allowed (unplug and plug again a device refused before)."
echo "USB disks and phones can be used from the team account."
else
# Removable media mounted while the restriction was off
findmnt -rno TARGET | grep '^/run/media/' | while read -r m; do
umount "$m" && echo "Unmounted $m"
done
move_rules on $USB_FILES
udevadm control --reload
systemctl enable --now usbguard.service
echo "USB restrictions on. The devices plugged in right now stay allowed until they are unplugged."
fi
}
network() {
if [ "$1" = off ]; then
move_rules off $NETWORK_FILES
echo "The team account can change the network settings."
else
move_rules on $NETWORK_FILES
echo "Network settings restricted to the administrators."
fi
}
state() {
if [ "$1" = yes ]; then echo "on"; else echo "OFF"; fi
}
status() {
fw=no; systemctl is-active --quiet firewall.service && fw=yes
us=no; systemctl is-active --quiet usbguard.service && [ -f /etc/polkit-1/rules.d/10-acpc-no-removable-media.rules ] && us=yes
nw=no; [ -f /etc/polkit-1/rules.d/10-acpc-no-network-changes.rules ] && nw=yes
echo "firewall $(state $fw)"
echo "usb $(state $us)"
echo "network $(state $nw)"
}
# What the machine refused: web sites, DNS names, USB devices
report() {
since=${1:-today}
echo "== Web sites refused by the proxy (since $since)"
journalctl -t contest_proxy --since "$since" --no-pager -o cat 2>/dev/null \
| awk '$3 != "200" || $4 == "-" {print $2}' | sort | uniq -c | sort -rn | head -40
echo
echo "== DNS names refused by the filter"
# Names that Firefox, VS Code, the IDEs and Kattis pages look up on their
# own in the background
background='(^|\.)(mozilla\.(com|org|net)|firefox-portal-detection\.com|googleapis\.com|gvt1\.com|microsoft\.com|vscode-cdn\.net|vscode-unpkg\.net|exp-tas\.com|openh264\.org|ipv4only\.arpa|sentry\.io|cloudflare-dns\.com|jetbrains\.com|ubuntu\.com|canonical\.com|snapcraft\.io)$'
names=$(journalctl -u contest-dns --since "$since" --no-pager -o cat 2>/dev/null \
| sed -n 's/^config \(.*\) is NXDOMAIN$/\1/p')
printf '%s\n' "$names" | grep -v -E "$background" | grep . | sort | uniq -c | sort -rn | head -40
echo " ($(printf '%s\n' "$names" | grep -c -E "$background") lookups by Firefox, VS Code and the IDEs in the background, not listed)"
echo
echo "== USB devices rejected"
journalctl -u usbguard --since "$since" --no-pager -o short 2>/dev/null \
| grep "target.new='reject'" \
| sed -E 's/^([A-Za-z]+ +[0-9]+ [0-9:]+).* name "([^"]*)".*/\1 \2/' | tail -20
}
case "${1:-}" in
status) status ;;
report) report "${2:-today}" ;;
on|off)
what=${2:-all}
case "$what" in
all) firewall "$1"; usb "$1"; network "$1" ;;
firewall|usb|network) "$what" "$1" ;;
*) usage ;;
esac
echo
status
;;
*) usage ;;
esac
@@ -0,0 +1,41 @@
#!/bin/bash
# Runs as root (contest-restrictions-warning.service). While any contest
# restriction is off (after maintenance with contest-restrictions off), shows
# a red banner across the top of the screen, so that the proctors notice it.
# The banner has no title bar or close button, the team account cannot stop
# this process, and the banner comes back within seconds if its window is
# destroyed. It is redrawn every 15 seconds to stay above the other windows.
export DISPLAY=:0 XAUTHORITY=/var/run/lightdm/root/:0
off() {
! systemctl is-active --quiet firewall.service ||
! systemctl is-active --quiet usbguard.service ||
[ -n "$(ls -A /etc/contest-restrictions/disabled 2>/dev/null)" ]
}
show() {
width=$(xdpyinfo 2>/dev/null | awk '/dimensions/ {split($2, a, "x"); print a[1]}')
[ -n "$width" ] || return
xmessage -geometry "${width}x70+0+0" -buttons "" \
-xrm "*overrideRedirect: true" \
-fg white -bg red3 -fn "-*-helvetica-bold-r-*-*-34-*-*-*-*-*-*-*" \
"CONTEST RESTRICTIONS ARE OFF ON THIS MACHINE - CALL THE STAFF" >/dev/null 2>&1 &
pid=$!
shown=$(date +%s)
}
pid=""
shown=0
while true; do
if off && [ -e "$XAUTHORITY" ]; then
if [ -z "$pid" ] || ! kill -0 "$pid" 2>/dev/null; then
show
elif [ $(( $(date +%s) - shown )) -ge 15 ]; then
old=$pid
show
sleep 1
kill "$old" 2>/dev/null
fi
elif [ -n "$pid" ]; then
kill "$pid" 2>/dev/null
pid=""
fi
sleep 3
done
@@ -0,0 +1,11 @@
[Unit]
Description=Red banner on the screen while a contest restriction is off
After=display-manager.service
[Service]
ExecStart=/usr/local/sbin/contest-restrictions-warning
Restart=always
RestartSec=3
[Install]
WantedBy=graphical.target
+38
View File
@@ -0,0 +1,38 @@
#!/bin/bash
# Manual staff command for permanently erasing the team account's files.
# Stops the desktop, deletes the home and team-owned files on the root
# filesystem, then recreates the home. No backup or undo.
# Never invoke from boot, login, a timer, cron, a playbook or automated tests.
# Run only after explicit staff authorization and verified backups.
# The --now argument is internal to the transient service after confirmation.
set -u
[ "$(id -u)" -eq 0 ] || { echo "Run this script as root (sudo)." >&2; exit 1; }
if [ "${1:-}" != --now ]; then
echo "The files of the team account will be deleted on this machine."
read -rp "Type RESET to continue: " answer
[ "$answer" = RESET ] || { echo "Cancelled."; exit 1; }
# The reset ends the desktop session, and with it this terminal if the
# script was started from the desktop. It runs as a separate service.
systemd-run --unit=reset-team-home --collect --quiet "$(readlink -f "$0")" --now
echo "Reset started. The screen restarts in a few seconds."
exit 0
fi
systemctl stop lightdm
loginctl terminate-user team 2>/dev/null
sleep 2
pkill -KILL -u team 2>/dev/null
sleep 1
for m in /home/team/.vscode/extensions /home/team/.vscode/extensions-root; do
umount "$m" 2>/dev/null
done
rm -rf --one-file-system /home/team
find / -xdev \( -path /proc -o -path /sys -o -path /dev \) -prune -o -user team -print0 2>/dev/null \
| xargs -0 -r rm -rf --one-file-system
mkdir /home/team
cp -a /etc/skel/. /home/team/
chown -R team:teams /home/team
chmod 750 /home/team
systemctl restart team-vscode-extensions.service
systemctl start lightdm
echo "The team account is clean."
+14
View File
@@ -0,0 +1,14 @@
[Unit]
Description=Wait for the clock before the desktop starts (at most 30 seconds)
After=chrony.service firewall.service contest-dns.service contest-proxy.service
Wants=chrony.service
Before=display-manager.service
[Service]
Type=oneshot
# Three checks 10 seconds apart, then the desktop starts anyway
ExecStart=-/usr/bin/chronyc waitsync 3 1
TimeoutStartSec=45
[Install]
WantedBy=multi-user.target
+10
View File
@@ -0,0 +1,10 @@
[Unit]
Description=Refresh the addresses of the allowlisted domains
After=firewall.service
[Service]
Type=oneshot
ExecStart=/refresh_rules.sh
[Install]
WantedBy=multi-user.target
+38
View File
@@ -0,0 +1,38 @@
#!/bin/bash
# Keeps the address set of the time servers up to date, in one atomic
# nftables transaction. NTP pools answer with different addresses at each
# query, so the addresses chrony actually picked are added to those resolved
# here. Web traffic does not need addresses: the local proxy connects by name.
CONF=/etc/contest-firewall
TMP=$(mktemp)
mkdir -p /run/contest-firewall
# One run at a time (the timer and a manual run can overlap)
exec 9> /run/contest-firewall/lock
flock 9
entries() {
sed -e 's/#.*//' -e 's/[[:space:]]//g' "$1" | grep -v '^$'
}
ntp_addresses() {
{
for name in $(entries "$CONF/ntp_servers.txt"); do
dig +short A "$name"
done
chronyc -n sources 2>/dev/null | awk '/^\^/ {print $2}'
} | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | sort -u
}
{
echo "flush set inet filter ntp_ips"
for ip in $(ntp_addresses); do
echo "add element inet filter ntp_ips { $ip }"
done
} > "$TMP"
# Apply atomically, then keep a copy of the active ruleset
nft -f "$TMP"
rm -f "$TMP"
nft list ruleset > /etc/nftables.rules
+9
View File
@@ -0,0 +1,9 @@
[Unit]
Description=Refresh the addresses of the allowlisted domains every 30 seconds
[Timer]
OnBootSec=0s
OnUnitActiveSec=30s
[Install]
WantedBy=timers.target
+19
View File
@@ -0,0 +1,19 @@
#!/bin/bash
# Prepares the VS Code extensions of the team account at boot, as root, with
# the script of the ICPC image. The image runs that script at login through
# sudo, which every account could then use as root. The paths it mounts and
# changes are checked first: a symbolic link there could point anywhere.
set -eu
USER_NAME=team
home=$(getent passwd "$USER_NAME" | cut -d: -f6)
for path in "$home/.pki" "$home/.pki/nssdb" "$home/.vscode" \
"$home/.vscode/extensions" "$home/.vscode/extensions-work" \
"$home/.vscode/extensions-overlay" "$home/.vscode/extensions-root"; do
if [ -L "$path" ]; then
logger -t team-vscode-extensions "removing the symbolic link $path"
rm -f "$path"
fi
done
[ -L "$home" ] && { echo "$home is a symbolic link" >&2; exit 1; }
export SUDO_UID=$(id -u "$USER_NAME") SUDO_GID=$(id -g "$USER_NAME")
exec /usr/local/bin/vscode-extension-install
@@ -0,0 +1,12 @@
[Unit]
Description=VS Code extensions of the team account
After=local-fs.target
Before=display-manager.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/sbin/team-vscode-extensions
[Install]
WantedBy=multi-user.target
+7
View File
@@ -0,0 +1,7 @@
[Unit]
Description=Set the clock from the contest site if NTP does not work
After=firewall.service chrony.service
[Service]
Type=oneshot
ExecStart=/time_from_contest.sh
+21
View File
@@ -0,0 +1,21 @@
#!/bin/bash
# Fallback for the clock. If chrony has no time server after two minutes of
# uptime (for instance when the campus network blocks NTP), the clock is set
# from the Date header of the contest site, which is accurate to the second.
CONF=/etc/contest-firewall
[ "$(cut -d. -f1 /proc/uptime)" -ge 120 ] || exit 0
if chronyc -n sources 2>/dev/null | grep -qE '^\^\*'; then
exit 0
fi
site=$(sed -e 's/#.*//' -e 's/[[:space:]]//g' "$CONF/allowed_domains.txt" | grep -m1 .)
# -k: certificates cannot be checked while the clock is far off
date_header=$(curl -skI --max-time 10 "https://$site/" | tr -d '\r' | sed -n 's/^[Dd]ate: //p')
[ -n "$date_header" ] || { logger -t time_from_contest "no answer from $site"; exit 1; }
server=$(date -d "$date_header" +%s) || exit 1
now=$(date +%s)
offset=$((server - now))
if [ "${offset#-}" -ge 2 ]; then
# The header has whole seconds: aim for the middle of the second
date -s "@$server.5" >/dev/null
logger -t time_from_contest "chrony has no time server; clock set from $site (was off by $offset s)"
fi
+9
View File
@@ -0,0 +1,9 @@
[Unit]
Description=Check every minute that the clock has a time source
[Timer]
OnBootSec=2min
OnUnitActiveSec=1min
[Install]
WantedBy=timers.target