New hotspot - pfSense
The following describes how to configure a pfSense firewall (version 2.7.2, Community Edition) and how to connect it to our hotspot solution SyCes.
For the installation of pfSense Community Edition (pfSense CE) or pfSense Plus software, the official documentation Netgate Docs is recommended.
Prerequisites
To configure a pfSense, you need administrator access to it. This tutorial demonstrates the configuration via the pfSense GUI web interface.
For the configuration, you also need information about the RADIUS IP and the secret of the new system. This information can be retrieved from SyCes🡕.
To set up the router for SyCes, the location managing this router must exist in the new database. The tenant ID and the token created for the location are required for the configuration. If you want to retrieve these values, you need valid credentials for SyCes.
Navigate to the location page of the corresponding tenant and click on the Copy
button next to the Token in the Token field on the Location Details card. The link in the URL field should look like this: /login/<tenant_id>/<location_id>/
.
Step 1: Set up the network interface
In the Interfaces > Assignment
tab, an interface needs to be configured first. The corresponding interface of the device should be equipped with a Wireless Access Point (WAP) so that customer devices can connect via the interface after configuration.
After clicking on Add
, a new interface is added. Then, the interface settings must be saved with Save
. Afterwards, the interface can be further configured by clicking on its name.
Enable the interface and optionally give it a descriptive name, e.g. GUESTNET. Set the IPv4 configuration to Static IPv4
and enter an IP address (e.g. 192.168.5.1) with a suitable network mask (e.g. /24).
Example interface settings can be seen in the following image:
Afterwards, the settings must be saved with Save
and applied with Apply Changes
.
Step 2: Set up the DHCP-Server
The DHCP server must now be configured for the newly created interface. This can be done in Services > DHCP Server
. If ISC DHCP is still set as the DHCP Backend
(factory settings), it is recommended to switch the DHCP backend to DHCP under System > Advanced > Networking
.
Select the created interface in the Services > DHCP Server
tab.
There, the DHCP server must be activated by clicking on the check mark. Then set the DHCP range for the IP addresses needed. Optionally, the DNS server can be set to 1.1.1.1 (Cloudflare) or 8.8.8.8 (Google).
Save the settings with Save
and apply them with Apply Changes
.
Step 3: Create Firewall Rules
In the next step, firewall rules must be created to allow access to the Captive Portal.
To do this, navigate to Firewall > Rules
.
Select the interface created in Step 2. Add a new firewall rule by clicking on Add
. This rule is used to allow access to the Captive Portal.
Set the rule to Pass
, the protocol to TCP
, the source to Interface Subnet
, the destination to Interface Address
, and enter the range 8000 to 10000 in the Destination port range
field. This means that traffic on all ports between 8000 and 10000 is allowed. In practice, however, usually only port 8000 is needed for the Captive Portal.
Optionally, logging can be enabled to monitor traffic and diagnose potential problems.
Save the settings with Save
and apply them with Apply Changes
.
Now add another rule to allow internet access for hotspot users. This rule must also be set to Pass
, the interface to the new interface (e.g. GUESTNET), the protocol to Any
, the source to Interface subnets
, and the destination to Any
. Optionally, you can add a description to this rule.
Save the settings with Save
and apply them with Apply Changes
.
Step 4: Set up the RADIUS Server
In this section, the RADIUS server is configured to the SyCes2 web server. First, it must be created in the User Manager
under System > User Manager
in the Authentication Servers
tab.
...
Click on Add
to add an authentication server.
Give the authentication server a name and select RADIUS
as the Type
. Additionally, you need to specify the RADIUS IP
and the RADIUS Secret
. This information can be found on SyCes.
Save your entries by clicking Save
.
Step 5: Set up the Captive Portal
Navigate to Services > Captive Portal
to set up the Captive Portal.
Add a new Captive Portal Zone by clicking Add
.
Enter a name for the Captive Portal Zone (e.g. GUESTNET). Optionally, you can enter a description for the zone. When done, click Save & Continue
.
Now activate the Captive Portal, select the GUESTNET interface and set a Idle timeout (e.g. 5 minutes).
Scroll to the Authentication
section, choose Use an Authentication backend
as the authentication method and select the server added in the previous step under Authentication Server
.
Now scroll to the Accounting
section, enable sending RADIUS accounting packets and select the configured RADIUS server as Accounting Server.
Select No updates
or Interim
updates, activate idle timeout and save the settings by clicking Save
.
Step 6: Testing the Captive Portal
Connect a device to the network set up in the previous steps and open a webpage in a browser to test the Captive Portal. Now, the Captive Portal should appear, allowing you to log in with your credentials.
The standard pfSense Captive Portal should look like this:
If you encounter any issues or unexpected behavior during the configuration of your router, please contact us at tsp-ms-dev@concat.de.
Step 7: Customizing the local Captive Portal (optional)
PfSense offers the option to upload a user-friendly portal page instead of using the predefined Captive Portal. To do this, edit the zone configured in Step 5 under Services > Captive Portal
.
Ensure that the checkbox for Enable Captive Portal
is selected. Then scroll to the Use custom captive portal page
option and enable the checkbox there as well. You can upload an HTML file via Browse
. Alternatively, you can use the webpage code provided below to set up a suggested page. Use View Page Contents
to preview the rendered page.
Other pages of the Captive Portal, as well as logos, can also be customized in this tab. Example of a custom Login Page are provided in the following section.
To allow hotspot devices to create user accounts on SyCes, the URL for the SyCes backend must be made accessible before authentication. Navigate to Services > Captive Portal
click on the Allowed Hostnames
tab and then click Add
to add a new entry. Enter backend.syces.de
under Hostname
. Optionally, you can add a description for the entry. The rest of the settings can remain at their default values. Save your changes by clicking Save
.
Login Page
Below, you will find the website code for the customized login page described above. You will also find the code for pages that use only the Anonymous or Login function. The German version of the login page is available under Neuer Hotspot - pfSense
This code can be copied for functionality, however, certain fields must still be replaced.
<tenant_id>
Replace this with your tenant's ID in SyCes<location_identification_token>
Replace this with the token of the configuredLocation
in SyCes with router typepfSense
. This is required for theAnonymous
function.const tenant_domain = "";
Set variable to enable automatic appending of the tenant domain to the username before login. Doing so enables the login with "account_name" instead of "account_name@tenant_domain". The domain can be copied from tenant page in SyCes.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SyCes® - Hotspot</title>
<link rel="stylesheet" href="https://backend.syces.de/static/login/css/styles.css">
</head>
<body>
<div class="message-container">
<div>
<img
src="https://backend.syces.de/static/login/img/logo2.png"
alt="SyCes-Logo"
width="500"
height="139"
class="img"
/>
</div>
<div class="center window">
<h1>User Login</h1>
<form id="default_login" action="$PORTAL_ACTION$" method="post">
<input name="redirurl" type="hidden" value="$PORTAL_REDIRURL$" />
<input name="zone" type="hidden" value="$PORTAL_ZONE$" />
<input name="accept" type="hidden" value="Continue" />
<table>
<tr>
<td><label for="pf_un">Username:</label></td>
<td><input name="auth_user" type="text" id="pf_un" /></td>
</tr>
<tr>
<td><label for="pf_pd">Password:</label></td>
<td><input name="auth_pass" type="password" id="pf_pd" /></td>
</tr>
<tr>
<td>
<label for="tos_check_login"
>I agree to the
<a href="https://backend.syces.de/login/<tenant_id>/tos/"
>Terms and conditions</a
>:</label
>
</td>
<td>
<label class="checkbox">
<input
type="checkbox"
name="tos"
id="tos_check_login"
onclick="handleTOSClick(this)"
/>
<span class="checkmark"></span>
</label>
</td>
</tr>
<tr>
<td></td>
<td>
<button
class="primary"
type="submit"
id="submit_btn_login"
disabled
>
Submit
</button>
</td>
</tr>
</table>
</form>
</div>
<div class="center window">
<h1>Register anonymously</h1>
<form id="anon_login">
<table>
<tr>
<td>
<label for="tos_check_reg">
I agree to the
<a href="https://backend.syces.de/login/<tenant_id>/tos/"
>Terms and conditions</a
>:
</label>
</td>
<td>
<label class="checkbox">
<input
type="checkbox"
name="tos"
id="tos_check_reg"
onclick="handleTOSClick(this)"
/>
<span class="checkmark"></span>
</label>
</td>
</tr>
<tr>
<td></td>
<td>
<button
class="primary"
type="submit"
id="submit_btn_reg"
disabled
>
Submit
</button>
</td>
</tr>
</table>
</form>
</div>
</div>
</body>
<script>
const url =
"https://backend.syces.de/login/token_login/<location_identification_token>/";
const anon_form = document.getElementById("anon_login");
const default_form = document.getElementById("default_login");
const tos_checkbox_reg = document.getElementById("tos_check_reg");
const tos_checkbox_login = document.getElementById("tos_check_login");
const submit_button_reg = document.getElementById("submit_btn_reg");
const submit_button_login = document.getElementById("submit_btn_login");
function handleTOSClick(cb) {
if (cb.checked === true) {
tos_checkbox_reg.checked = true;
tos_checkbox_login.checked = true;
submit_button_reg.disabled = false;
submit_button_login.disabled = false;
} else {
tos_checkbox_reg.checked = false;
tos_checkbox_login.checked = false;
submit_button_reg.disabled = true;
submit_button_login.disabled = true;
}
}
function sendRequest() {
fetch(url)
.then((response) => response.json())
.then((data) => {
let field_username = document.getElementById("pf_un");
let field_password = document.getElementById("pf_pd");
field_username.value = data.login_name;
field_password.value = data.password;
default_form.submit();
})
.catch((error) => console.error(error));
}
function appendDomainIfMissing() {
const tenant_domain = "";
let form_input_user = document.getElementById("ft_un");
let username = form_input_user.value;
if (
tenant_domain !== "" &&
!username.includes("@") &&
!username.endsWith("@" + tenant_domain)
) {
form_input_user.value = username + "@" + tenant_domain;
}
}
anon_form.addEventListener("submit", function (e) {
e.preventDefault();
sendRequest();
});
default_form.addEventListener("submit", function (e) {
appendDomainIfMissing();
});
</script>
</html>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SyCes® - Hotspot</title>
<link rel="stylesheet" href="https://backend.syces.de/static/login/css/styles.css">
</head>
<body>
<div class="message-container">
<div>
<img
src="https://backend.syces.de/static/login/img/logo2.png"
alt="SyCes-Logo"
width="500"
height="139"
class="img"
/>
</div>
<div class="center window">
<h1>User Login</h1>
<form id="default_login" action="$PORTAL_ACTION$" method="post">
<input name="redirurl" type="hidden" value="$PORTAL_REDIRURL$" />
<input name="zone" type="hidden" value="$PORTAL_ZONE$" />
<input name="accept" type="hidden" value="Continue" />
<table>
<tr>
<td><label for="pf_un">Username:</label></td>
<td><input name="auth_user" type="text" id="pf_un" /></td>
</tr>
<tr>
<td><label for="pf_pd">Password:</label></td>
<td><input name="auth_pass" type="password" id="pf_pd" /></td>
</tr>
<tr>
<td>
<label for="tos_check_login">
I agree to the
<a href="https://backend.syces.de/login/<tenant_id>/tos/">
Terms and conditions
</a>:
</label>
</td>
<td>
<label class="checkbox">
<input
type="checkbox"
name="tos"
id="tos_check_login"
onclick="handleTOSClick(this)"
/>
<span class="checkmark"></span>
</label>
</td>
</tr>
<tr>
<td></td>
<td>
<button
class="primary"
type="submit"
id="submit_btn_login"
disabled
>
Submit
</button>
</td>
</tr>
</table>
</form>
</div>
</div>
</body>
<script>
const default_form = document.getElementById("default_login");
const tos_checkbox_login = document.getElementById("tos_check_login");
const submit_button_login = document.getElementById("submit_btn_login");
function handleTOSClick(cb) {
if (cb.checked === true) {
tos_checkbox_login.checked = true;
submit_button_login.disabled = false;
} else {
tos_checkbox_login.checked = false;
submit_button_login.disabled = true;
}
}
function appendDomainIfMissing() {
const tenant_domain = "";
let form_input_user = document.getElementById("ft_un");
let username = form_input_user.value;
if (
tenant_domain !== "" &&
!username.includes("@") &&
!username.endsWith("@" + tenant_domain)
) {
form_input_user.value = username + "@" + tenant_domain;
}
}
default_form.addEventListener("submit", function (e) {
appendDomainIfMissing();
});
</script>
</html>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SyCes® - Hotspot</title>
<link rel="stylesheet" href="https://backend.syces.de/static/login/css/styles.css">
</head>
<body>
<div class="message-container">
<div>
<img
src="https://backend.syces.de/static/login/img/logo2.png"
alt="SyCes-Logo"
width="500"
height="139"
class="img"
/>
</div>
<div class="center window">
<h1>Register anonymously</h1>
<form id="anon_login">
<table>
<tr>
<td>
<label for="tos_check_reg">
I agree to the
<a href="https://backend.syces.de/login/<tenant_id>/tos/">
Terms and conditions
</a>:
</label>
</td>
<td>
<label class="checkbox">
<input
type="checkbox"
name="tos"
id="tos_check_reg"
onclick="handleTOSClick(this)"
/>
<span class="checkmark"></span>
</label>
</td>
</tr>
<tr>
<td></td>
<td>
<button
class="primary"
type="submit"
id="submit_btn_reg"
disabled
>
Submit
</button>
</td>
</tr>
</table>
</form>
<form
id="default_login"
action="$PORTAL_ACTION$"
method="post"
style="display:none"
>
<input name="redirurl" type="hidden" value="$PORTAL_REDIRURL$" />
<input name="zone" type="hidden" value="$PORTAL_ZONE$" />
<input name="accept" type="hidden" value="Continue" />
<h1>
Login
</h1>
<table>
<tr>
<td><label for="pf_un">Username:</label></td>
<td><input name="auth_user" type="text" id="pf_un" /></td>
</tr>
<tr>
<td><label for="pf_pd">Password:</label></td>
<td><input name="auth_pass" type="password" id="pf_pd" /></td>
</tr>
<tr>
<td>
<label for="tos_check_login"
>I agree to the
<a href="https://backend.syces.de/login/<tenant_id>/tos/"
>Terms and conditions</a
>:</label
>
</td>
<td>
<label class="checkbox">
<input
type="checkbox"
name="tos"
id="tos_check_login"
onclick="handleTOSClick(this)"
/>
<span class="checkmark"></span>
</label>
</td>
</tr>
<tr>
<td></td>
<td>
<button
class="primary"
type="submit"
id="submit_btn_login"
disabled
>
Submit
</button>
</td>
</tr>
</table>
</form>
</div>
</div>
</body>
<script>
const url =
"https://backend.syces.de/login/token_login/<location_identification_token>/";
const anon_form = document.getElementById("anon_login");
const default_form = document.getElementById("default_login");
const tos_checkbox_reg = document.getElementById("tos_check_reg");
const tos_checkbox_login = document.getElementById("tos_check_login");
const submit_button_reg = document.getElementById("submit_btn_reg");
const submit_button_login = document.getElementById("submit_btn_login");
function handleTOSClick(cb) {
if (cb.checked === true) {
tos_checkbox_reg.checked = true;
tos_checkbox_login.checked = true;
submit_button_reg.disabled = false;
submit_button_login.disabled = false;
} else {
tos_checkbox_reg.checked = false;
tos_checkbox_login.checked = false;
submit_button_reg.disabled = true;
submit_button_login.disabled = true;
}
}
function sendRequest() {
fetch(url)
.then(response => response.json())
.then(data => {
let field_username = document.getElementById("pf_un");
let field_password = document.getElementById("pf_pd");
field_username.value = data.login_name;
field_password.value = data.password;
default_form.submit();
}
)
.catch(error => console.error(error));
}
anon_form.addEventListener("submit", function(e) {
e.preventDefault();
sendRequest();
});
</script>
</html>
Use Restore Default Page
to reset settings back to the predefined Captive Portal configuration.