One place for hosting & domains

      How To Configure Packet Filter (PF) on FreeBSD 12.1


      The author selected the COVID-19 Relief Fund to receive a donation as part of the Write for DOnations program.

      Introduction

      The firewall is arguably one of the most important lines of defense against cyber attacks. The ability to configure a firewall from scratch is an empowering skill that enables the administrator to take control of their networks.

      Packet Filter (PF) is a renown firewall application that is maintained upstream by the security-driven OpenBSD project. It is more accurately expressed as a packet filtering tool, hence the name, and it is known for its simple syntax, user-friendliness, and extensive features. PF is a stateful firewall by default, storing information about connections in a state table that can be accessed for analytical purposes. PF is part of the FreeBSD base system and is supported by a strong community of developers. Although there are differences between the FreeBSD and OpenBSD versions of PF related to kernel architectures, in general their syntax is similar. Depending on their complexity, common rulesets can be modified to work on either distribution with relatively little effort.

      In this tutorial you’ll build a firewall from the ground up on a FreeBSD 12.1 server with PF. You’ll design a base ruleset that can be used as a template for future projects. You’ll also explore some of PF’s advanced features such as packet hygiene, brute force prevention, monitoring and logging, and other third-party tools.

      Prerequisites

      Before you start this tutorial, you’ll need the following:

      • A 1G FreeBSD 12.1 server (either ZFS or UFS). You can use our How To Get Started with FreeBSD tutorial to set your server up to your preferred configuration.
      • FreeBSD has no firewall enabled by default—customization is a hallmark of the FreeBSD ethos. Therefore when you first launch your server, you need temporary protection while PF is being configured. If you’re using DigitalOcean, you can enable your cloud firewall immediately after spinning up the server. Refer to DigitalOcean’s Firewall Quickstart for instructions on configuring a cloud firewall. If you’re using another cloud provider, determine the fastest route to immediate protection before you begin. Whichever method you choose, your temporary firewall must permit only inbound SSH traffic, and can allow all types of outbound traffic.

      Step 1 — Building Your Preliminary Ruleset

      You’ll begin this tutorial by drafting a preliminary ruleset that provides basic protection and access to critical services from the internet. At this point you have a running FreeBSD 12.1 server with an active cloud firewall.

      There are two approaches to building a firewall: default deny and default permit. The default deny approach blocks all traffic, and only permits what is specified in a rule. The default permit approach does the exact opposite: it passes all traffic, and only blocks what is specified in a rule. You’ll use the default deny approach.

      PF rulesets are written in a configuration file named /etc/pf.conf, which is also its default location. It is OK to store this file somewhere else as long as it is specified in the /etc/rc.conf configuration file. In this tutorial you’ll use the default location.

      Log in to your server with your non-root user:

      • ssh freebsd@your_server_ip

      Next create your /etc/pf.conf file:

      Note: If you would like to see the complete base ruleset at any point in the tutorial, you can refer to the examples in Step 4 or Step 8.

      PF filters packets according to three core actions: block, pass, and match. When combined with other options they form rules. An action is taken when a packet meets the criteria specified in a rule. As you may expect, pass and block rules will pass and block traffic. A match rule performs an action on a packet when it finds a matching criteria, but doesn’t pass or block it. For example, you can perform network address translation (NAT) on a matching packet without passing or blocking it, and it will sit there until you tell it to do something in another rule, such as route it to another machine or gateway.

      Next add the first rule to your /etc/pf.conf file:

      /etc/pf.conf

      block all
      

      This rule blocks all forms of traffic in every direction. Since it does not specify a direction, it defaults to both in and out. This rule is legitimate for a local workstation that needs to be insulated from the world, but it is largely impractical, and will not work on a remote server because it does not permit SSH traffic. In fact, had you enabled PF, you would have locked yourself out of the server.

      Revise your /etc/pf.conf file to allow SSH traffic with the following highlighted line:

      /etc/pf.conf

      block all
      pass in proto tcp to port 22
      

      Note: Alternatively, you can use the name of the protocol:

      /etc/pf.conf

      block all
      pass in proto tcp to port ssh
      

      For the sake of consistency we will use port numbers, unless there is a valid reason not to. There is a detailed list of protocols and their respective port numbers in the /etc/services file, which you are encouraged to view.

      PF processes rules sequentially from top-to-bottom, therefore your current ruleset initially blocks all traffic, but then passes it if the criteria on the next line is matched, which in this case is SSH traffic.

      You can now SSH in to your server, but you’re still blocking all forms of outbound traffic. This is problematic because you can’t access critical services from the internet to install packages, update your time settings, and so on.

      To address this, append the following highlighted rule to the end of your /etc/pf.conf file:

      /etc/pf.conf

      block all
      pass in proto tcp to port { 22 }
      pass out proto { tcp udp } to port { 22 53 80 123 443 }
      

      Your ruleset now permits outbound SSH, DNS, HTTP, NTP, and HTTPS traffic, as well as blocking all inward traffic, (with the exception of SSH). You place the port numbers and protocols inside curly brackets, which forms a list in PF syntax, allowing you to add more port numbers if needed. You also add a pass out rule for the UDP protocol on ports 53 and 123 because DNS and NTP often toggle between both the TCP and UDP protocols. You’re almost finished with the preliminary ruleset, and only need to add a couple of rules to achieve basic functionality.

      Complete the preliminary ruleset with the highlighted rules:

      Preliminary Ruleset /etc/pf.conf

      set skip on lo0
      block all
      pass in proto tcp to port { 22 }
      pass out proto { tcp udp } to port { 22 53 80 123 443 }
      pass out inet proto icmp icmp-type { echoreq }
      

      Save and exit the file.

      You create a set skip rule for the loopback device because it does not need to filter traffic and would likely bring your server to a crawl. You add a pass out inet rule for the ICMP protocol, which allows you to use the ping(8) utility for troubleshooting. The inet option represents the IPv4 address family.

      ICMP is a multi-purpose messaging protocol used by networking devices for various types of communication. The ping utility for example uses a type of message known as an echo request, which you’ve added to your icmp_type list. As a precaution, you only permit the message types that you need to prevent unwelcome devices from contacting your server. As your needs increase you can add more message types to your list.

      You now have a working ruleset that provides basic functionality to most machines. In the next section, let’s confirm that everything is working correctly by enabling PF and testing your preliminary ruleset.

      Step 2 — Testing Your Preliminary Ruleset

      In this step you’ll test your preliminary ruleset and make the transition from your cloud firewall to your PF firewall, allowing PF to completely take over. You’ll activate your ruleset with the pfctl utility, which is PF’s built-in command-line tool, and the primary method of interfacing with PF.

      PF rulesets are nothing more than text files, which means there are no delicate procedures involved with loading new rulesets. You can load a new ruleset, and the old one is gone. There is rarely, if ever, a need to flush an existing ruleset.

      FreeBSD uses a web of shell scripts known as the rc system to manage how services are started at boot-time; we specify those services in various rc configuration files. For global services such as PF, you use the /etc/rc.conf file. Since rc files are critical to the well being of a FreeBSD system, they should not be edited directly. Instead FreeBSD provides a command-line utility known as sysrc designed to help you edit these files safely.

      Let’s enable PF using the sysrc command-line utility:

      • sudo sysrc pf_enable="YES"
      • sudo sysrc pflog_enable="YES"

      Verify these changes by printing the contents of your /etc/rc.conf file:

      You will see the following output:

      Output

      pf_enable="YES" pflog_enable="YES"

      You also enable the pflog service, which in turn, enables the pflogd daemon for logging in PF.(You’ll work with logging in a later step.

      You specify two global services in your /etc/rc.conf file, but they won’t initialize until you reboot the server or start them manually. Reboot the server so that you can also test your SSH access.

      Start PF by rebooting the server:

      The connection will be dropped. Give it a few minutes to update.

      Now SSH back in to the server:

      • ssh freebsd@your_server_ip

      Although you’ve initialized your PF services, you haven’t actually loaded your /etc/pf.conf ruleset, which means your firewall is not yet active.

      Load the ruleset with pfctl:

      • sudo pfctl -f /etc/pf.conf

      If there are no errors or messages, it means your ruleset has no errors and the firewall is active.

      Now that PF is running, you can detach your server from your cloud firewall. This can be accomplished at the control panel in your DigitalOcean account by removing your Droplet from your cloud firewall’s portal. If you’re using another cloud provider, ensure that whatever you are using for temporary protection is disabled. Running two different firewalls on a server will almost certainly cause problems.

      For good measure, reboot your server again:

      After a few minutes, SSH back in to your server:

      • ssh freebsd@your_server_ip

      PF is now your acting firewall. You can ensure that it is running by accessing some data with the pfctl utility.

      Let’s view some statistics and counters with pfctl -si:

      You pass the -si flags, which stand for show info. This is one of the many filter parameter combinations you can use with pfctl to parse data about your firewall activity.

      You will see the following tabular data (the values will vary from machine-to-machine):

      Output

      Status: Enabled for 0 days 00:01:53 Debug: Urgent State Table Total Rate current entries 5 searches 144 1.3/s inserts 11 0.1/s removals 6 0.1/s Counters match 23 0.2/s bad-offset 0 0.0/s fragment 0 0.0/s short 0 0.0/s normalize 0 0.0/s memory 0 0.0/s bad-timestamp 0 0.0/s congestion 0 0.0/s ip-option 0 0.0/s proto-cksum 0 0.0/s state-insert 0 0.0/s state-limit 0 0.0/s src-limit 0 0.0/s synproxy 0 0.0/s map-failed 0 0.0/s

      Since you just activated your ruleset, you won’t see a lot of information yet. However this output shows that PF already recorded 23 matched rules, meaning that the criteria of your ruleset was matched 23 times. The output also confirms that your firewall is working.

      Your ruleset also permits outbound traffic to access some critical services from the internet, including the ping utility.

      Let’s check for internet connectivity and DNS service with ping against google.com:

      Since you ran the count flag -c 3, you’ll see three successful connection responses:

      Output

      PING google.com (172.217.0.46): 56 data bytes 64 bytes from 172.217.0.46: icmp_seq=0 ttl=56 time=2.088 ms 64 bytes from 172.217.0.46: icmp_seq=1 ttl=56 time=1.469 ms 64 bytes from 172.217.0.46: icmp_seq=2 ttl=56 time=1.466 ms --- google.com ping statistics --- 3 packets transmitted, 3 packets received, 0.0% packet loss round-trip min/avg/max/stddev = 1.466/1.674/2.088/0.293 ms

      Ensure that you can access the the pkgs repository with the following command:

      If there are any packages to upgrade, go ahead and upgrade them.

      If both of these services are working, it means your firewall is working and you can now proceed. Although your preliminary ruleset provides protection and functionality, it is still an elementary ruleset, and could use some enhancements. In the remaining sections you’ll complete your base ruleset, and use some of PF’s advanced features.

      Step 3 — Completing Your Base Ruleset

      In this step you’ll build off of the preliminary ruleset to complete your base ruleset. You’ll reorganize some of your rules and work with more advanced concepts.

      Incorporating Macros and Tables

      In your preliminary ruleset you hard coded all of your parameters into each rule, that is, the port numbers that make up the lists. This may become unmanageable in the future, depending on the nature of your networks. For organizational purposes PF includes macros, lists, and tables. You’ve already included lists directly in your rules, but you can also separate them from your rules and assign them to a variable using macros.

      Open your file to transfer some of your parameters into macros:

      Now add the following content to the very top of the ruleset:

      /etc/pf.conf

      vtnet0 = "vtnet0"
      icmp_types = "{ echoreq }"
      . . .
      

      Modify your previous SSH and ICMP rules with your new variables:

      /etc/pf.conf

      . . .
      pass in on $vtnet0 proto tcp to port { 22 }
      . . .
      pass inet proto icmp icmp-type $icmp_types
      . . .
      

      Your previous SSH and ICMP rules now use macros. The variable names are denoted by PF’s dollar sign syntax. You assign your vtnet0 interface to a variable with the same name just as a formality, which gives you the option to rename it in the future if needed. Other common variable names for public facing interfaces include $pub_if or $ext_if.

      Next you’ll implement a table, which is similar to a macro, but designed to hold groups of IP addresses. Let’s create a table for non-routable IP addresses, which often play a role in denial of service attacks (DOS). You can use the IP addresses specified in RFC6890, which defines special-purpose IP address registries. Your server should not send or receive packets to or from these addresses via the public facing interface.

      Create this table by adding the following content directly under the icmp_types macro:

      /etc/pf.conf

      . . .
      table <rfc6890> { 0.0.0.0/8 10.0.0.0/8 100.64.0.0/10 127.0.0.0/8 169.254.0.0/16          
                        172.16.0.0/12 192.0.0.0/24 192.0.0.0/29 192.0.2.0/24 192.88.99.0/24    
                        192.168.0.0/16 198.18.0.0/15 198.51.100.0/24 203.0.113.0/24            
                        240.0.0.0/4 255.255.255.255/32 }
      . . .
      

      Now add your rules for the <rfc6890> table underneath the set skip on lo0 rule:

      /etc/pf.conf

      . . .
      set skip on lo0
      block in quick on egress from <rfc6890>
      block return out quick on egress to <rfc6890>
      . . .
      

      Here you introduce the return option, which complements your block out rule. This will drop the packets and also send an RST message to the host that tried to make those connections, which is useful for analyzing host activity. Then, you add the egress keyword, which automatically finds the default route(s) on any given interface(s). This is typically a cleaner method of finding default routes, especially with complex networks. The quick keyword executes rules immediately without considering the rest of the ruleset. For example, if a packet with an illogical IP addresses tries to connect to the server, you want to drop the connection immediately, and have no reason to run that packet through the remainder of the ruleset.

      Protecting Your SSH Ports

      Since your SSH port is open to the public, it is subject to exploitation. One of the more obvious warning signs of an attacker is mass quantities of log-in attempts. For example if the same IP address tries to log in to your server ten times in one second, you can assume that it was not done with human hands, but with computer software that was trying to crack your login password. These types of systematic exploits are often referred to as brute force attacks, and usually succeed if the server has weak passwords.

      Warning: We strongly recommend using public-key authentication on all servers. Refer to DigitalOcean’s tutorial on key-based authentication.

      PF has built-in features for handling brute force and other similar attacks. With PF you can limit the number of simultaneous connection attempts allowed by a single host. If a host exceeds those limits, the connection will be dropped, and they will be banned from the server. To accomplish this you’ll use PF’s overload mechanism, which maintains a table of banned IP addresses.

      Modify your previous SSH rule to limit the number of simultaneous connections from a single host as per the following:

      /etc/pf.conf

      . . .
      pass in on $vtnet0 proto tcp to port { 22 } 
          keep state (max-src-conn 15, max-src-conn-rate 3/1, 
              overload <bruteforce> flush global)
      . . .
      

      You add the keep state option that allows you to define the state criteria for the overload table. You pass the max-src-conn parameter to specify the number of simultaneous connections allowed from a single host per second, and the max-src-conn-rate parameter to specify the number of new connections allowed from a single host per second. You specify 15 connections for max-src-conn, and 3 connections for max-src-conn-rate. If these limits are exceeded by a host, the overload mechanism adds the source IP to the <bruteforce> table, which bans them from the server. Finally, the flush global option immediately drops the connection.

      You’ve defined an overload table in your SSH rule, but haven’t declared that table in your ruleset.

      Add the <bruteforce> table underneath the icmp_types macro:

      /etc/pf.conf

      . . .
      icmp_types = "{ echoreq }"
      table <bruteforce> persist
      . . .
      

      The persist keyword allows an empty table to exist in the ruleset. Without it, PF will complain that there are no IP addresses in the table.

      These measures ensure that your SSH port is protected by a powerful security mechanism. PF allows you to configure quick solutions to protect from disastrous forms of exploitation. In the next sections you’ll take steps to clean up packets as they arrive at your server.

      Sanitizing Your Traffic

      Note: The following sections describe basic fundamentals of the TCP/IP protocol suite. If you plan on building web applications or networks, it is in your best interest to master these concepts. Have a look at DigitalOcean’s Introduction to Networking Terminology, Interfaces, and Protocols tutorial.

      Due to the complexity of the TCP/IP protocol suite, and the perserverance of malicious actors, packets often arrive with discrepancies and ambiguities such as overlapping IP fragments, phony IP addresses, and more. It is imperative that you sanitize your traffic before it enters the system. The technical term for this process is normalization.

      When data travels through the internet, it is typically broken up into smaller fragments at its source to accommodate for the transmission parameters of the target host, where it is reassembled into complete packets. Unfortunately an intruder can hijack this process in a number of ways that span beyond the scope of this tutorial. However, with PF you can manage fragmentation with one rule. PF includes a scrub keyword that normalizes packets.

      Add the scrub keyword directly preceding your block all rule:

      /etc/pf.conf

      . . .
      set skip on lo0
      scrub in all fragment reassemble max-mss 1440
      block all
      . . .
      

      This rule applies scrubbing to all incoming traffic. You include the fragment reassemble option that prevents fragments from entering the system. Instead they are cached in memory until they are reassembled into complete packets, which means your filter rules will only have to contend with uniform packets. You also include the max-mss 1440 option, which represents the maximum segment size of reassembled TCP packets, also known as the payload. You specify a value of 1440 bytes, which strikes a balance between size and performance, leaving plenty of room for the headers.

      Another important aspect of fragmentation is a term known as the maximum transmission unit (MTU). The TCP/IP protocols enable devices to negotiate packet sizes for making connections. The target host uses ICMP messages to inform the source IP of its MTU, a process known as MTU path discovery. The specific ICMP message type is the destination unreachable. You’ll enable MTU path discovery by adding the unreach message type to your icmp_types list.

      You’ll use your server’s default MTU of 1500 bytes, which can be determined with the ifconfig command:

      You will see the following output that includes your current MTU:

      Output

      vtnet0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500 options=6c07bb<RXCSUM,TXCSUM,VLAN_MTU,VLAN_HWTAGGING,JUMBO_MTU,VLAN_HWCSUM,TSO4,TSO6,LRO,VLAN_HWTSO,LINKSTATE,RXCSUM_IPV6,TXCSUM_IPV6> . . .

      Update the icmp_types list to include the destination unreachable message type:

      /etc/pf.conf

      vtnet0 = "vtnet0"
      icmp_types = "{ echoreq unreach}"
      . . .
      

      Now that you have policies in place to handle fragmentation, the packets that enter your system will be uniform and consistent. This is desirable because there are so many devices exchanging data over the internet.

      You’ll now work to prevent another security concern known as IP spoofing. Attackers often change their source IPs to make it appear as if they reside on a trusted node within an organization. PF includes an antispoofing directive for handling spoofed source IPs. When applied to a specific interface(s), antispoofing blocks all traffic from the network of that interface (unless it originates from that interface). For example, if you apply antispoofing to an interface(s) that resides at 5.5.5.1/24, all traffic from the 5.5.5.0/24 network cannot communicate with the system unless it originates from that interface(s).

      Add the following highlighted content to apply antispoofing to your vtnet0 interface:

      /etc/pf.conf

      . . .
      set skip on lo0
      scrub in
      antispoof quick for $vtnet0
      block all
      . . .
      

      Save and exit the file.

      This antispoofing rule says that all traffic from vtnet0’s network(s) can only pass through the vtnet0 interface, or it will be dropped immediately with the quick keyword. Bad actors will not be able to hide in vtnet0’s network and communicate with other nodes.

      To demonstrate your antispoofing rule, you’ll print your ruleset to the screen in its verbose form. Rules in PF are typically written in a shortened form, but they can also be written in a verbose form. It is generally impractical to write rules this way, but for testing purposes it can be useful.

      Print the contents of /etc/pf.conf using pfctl with the following command:

      • sudo pfctl -nvf /etc/pf.conf

      This pfctl command takes the -nvf flags, which print the ruleset and test it without actually loading anything, also known as a dry run. You will now see the entire contents of /etc/pf.conf in its verbose form.

      You’ll see something similar to the following output within the antispoofing portion:

      Output

      . . . block drop in quick on ! vtnet0 inet from your_server_ip/20 to any block drop in quick on ! vtnet0 inet from network_address/16 to any block drop in quick inet from your_server_ip to any block drop in quick inet from network_address to any block drop in quick on vtnet0 inet6 from your_IPv6_address to any . . .

      Your antispoofing rule discovered that it is part of the your_server_ip/20 network. It also detected that (for this tutorial’s example) the server is part of a network_address/16 network, and has an additional IPv6 address. Antispoofing blocks all of these networks from communicating with the system, unless their traffic passes through the vtnet0 interface.

      Your antispoofing rule is the last addition to your base ruleset. In the next step you’ll initiate these changes and perform some testing.

      Step 4 — Testing Your Base Ruleset

      In this step you’ll review and test your base ruleset to ensure that everything is functioning properly. It’s best to avoid implementing too many rules at once without testing them. Best practice is to start with the essentials, expand incrementally, and back work up while making configuration changes.

      Here is your complete base ruleset:

      Base Ruleset /etc/pf.conf

      vtnet0 = "vtnet0"
      icmp_types = "{ echoreq unreach }"
      table <bruteforce> persist
      table <rfc6890> { 0.0.0.0/8 10.0.0.0/8 100.64.0.0/10 127.0.0.0/8 169.254.0.0/16          
                        172.16.0.0/12 192.0.0.0/24 192.0.0.0/29 192.0.2.0/24 192.88.99.0/24    
                        192.168.0.0/16 198.18.0.0/15 198.51.100.0/24 203.0.113.0/24            
                        240.0.0.0/4 255.255.255.255/32 }
      
      set skip on lo0
      scrub in all fragment reassemble max-mss 1440
      antispoof quick for $vtnet0
      block in quick on $vtnet0 from <rfc6890>
      block return out quick on egress to <rfc6890>
      block all
      pass in on $vtnet0 proto tcp to port { 22 } 
          keep state (max-src-conn 15, max-src-conn-rate 3/1, 
              overload <bruteforce> flush global)
      pass out proto { tcp udp } to port { 22 53 80 123 443 }
      pass inet proto icmp icmp-type $icmp_types
      

      Be sure that your /etc/pf.conf file is identical to the complete base ruleset here before continuing. Then save and exit the file.

      Your complete base ruleset provides you with:

      • A collection of macros that can define key services and devices.
      • Network hygiene policies to address packet fragmentation and illogical IP addresses.
      • A default deny filtering structure that blocks everything and permits only what you specify.
      • Inbound SSH access with limits on the number of simultaneous connections that can be made by a host.
      • Outbound traffic policies that give you access to some critical services from the internet.
      • ICMP policies that provide access to the ping utility and MTU path discovery.

      Run the following pfctl command to take a dry run:

      • sudo pfctl -nf /etc/pf.conf

      You pass the -nf flags that tell pfctl to run the ruleset without loading it, which will throw errors if anything is wrong.

      Now, with no encountered errors, load the ruleset:

      • sudo pfctl -f /etc/pf.conf

      If there are no errors, it means your base ruleset is active and functioning properly. As earlier in the tutorial, you’ll perform a few tests on your ruleset.

      First test for internet connectivity and DNS service:

      You will see the following output:

      Output

      PING google.com (172.217.0.46): 56 data bytes 64 bytes from 172.217.0.46: icmp_seq=0 ttl=56 time=2.088 ms 64 bytes from 172.217.0.46: icmp_seq=1 ttl=56 time=1.469 ms 64 bytes from 172.217.0.46: icmp_seq=2 ttl=56 time=1.466 ms --- google.com ping statistics --- 3 packets transmitted, 3 packets received, 0.0% packet loss round-trip min/avg/max/stddev = 1.466/1.674/2.088/0.293 ms

      Then, check that you reach the pkgs repository:

      Once again, upgrade packages if it’s needed.

      Finally, reboot your server:

      Give your server a few minutes to reboot. You’ve completed and implemented your base ruleset, which is a significant step in terms of your progress. You’re now ready to explore some of PF’s advanced features. In the next step you will continue to prevent brute force attacks.

      Step 5 — Managing Your Overload Table

      Over time the <bruteforce> overload table will become full of malicious IP addresses and will need to be cleared periodically. It is unlikely that an attacker will continue using the same IP address, so it is counterintuitive to store them in the overload table for long periods of time.

      You’ll use pfctl to manually clear IP addresses that have been stored in the overload table for 48 hours or more with the following command:

      • sudo pfctl -t bruteforce -T expire 172800

      You will see output similar to:

      Output

      0/0 addresses expired.

      You pass the -t bruteforce flag, which stands for table bruteforce, and the -T flag, which lets you run a handful of built-in commands. In this case you run the expire command to clear all entries from -t bruteforce with a time value represented in seconds. Since you’re working on a fresh server, there are probably no IP addresses in the overload table yet.

      This rule works for quick fixes, but a more robust solution would be to automate the process with cron, FreeBSD’s job scheduler. Let’s create a shell script that runs this command sequence instead.

      Create a shell script file in the /usr/local/bin directory:

      • sudo vi /usr/local/bin/clear_overload.sh

      Add the following content to the shell script:

      /usr/local/bin/clear_overload.sh

      #!/bin/sh
      
      pfctl -t bruteforce -T expire 172800
      

      Make the file executable with the following command:

      • sudo chmod 755 /usr/local/bin/clear_overload.sh

      Next you’ll create a cron job. These are jobs that will run repetitively according to a time that you specify. They are commonly used for backups, or any process that needs to run at the same time every day. You create cron jobs with crontab files. Please refer to the man pages to learn more about cron(8) and crontab(5).

      Create a root user crontab file with the following command:

      Now add the following contents to the crontab file:

      crontab

      # minute    hour    mday    month   wday    command
      
        *             0     *       *     *     /usr/local/bin/clear_overload.sh
      

      Save and exit the file.

      Note: Please align every value to its corresponding table entry for readability if things do not align properly when you add the content.

      This cron job runs the clear_overload.sh script every day at midnight, removing IP addresses that are 48 hours old from the overload table <bruteforce>. Next you’ll add anchors to your ruleset.

      Step 6 — Introducing Anchors to Your Rulesets

      In this step you’ll introduce anchors, which are used for sourcing rules into the main ruleset, either manually or from an external text file. Anchors can contain rule snippets, tables, and even other anchors, known as nested anchors. Let’s demonstrate how anchors work by adding a table to an external file, and sourcing it into your base ruleset. Your table will include a group of internal hosts that you want to prevent from connecting to the outside world.

      Create a file named /etc/blocked-hosts-anchor:

      • sudo vi /etc/blocked-hosts-anchor

      Add the following contents to the file:

      /etc/blocked-hosts-anchor

      table <blocked-hosts> { 192.168.47.1 192.168.47.2 192.168.47.3 }
      
      block return out quick on egress from <blocked-hosts>
      

      Save and exit the file.

      These rules declare and define the <blocked-hosts> table, and then prevent every IP address in the <blocked-hosts> table from accessing services from the outside world. You use the egress keyword as a preferred method of finding the default route, or way out, to the internet.

      You still need to declare the anchor in your /etc/pf.conf file:

      Now add the following anchor rules after the block all rule:

      /etc/pf.conf

      . . .
      block all
      anchor blocked_hosts
      load anchor blocked_hosts from "/etc/blocked-hosts-anchor"
      . . .
      

      Save and exit the file.

      These rules declare the blocked_hosts and load the anchor rules into your main ruleset from the /etc/blocked-hosts-anchor file.

      Now initiate these changes by reloading your ruleset with pfctl:

      • sudo pfctl -f /etc/pf.conf

      If there are no errors, it means that there are no errors in your ruleset and your changes are active.

      Use pfctl to verify that your anchor is running:

      The -s Anchors flag stands for “show anchors”. You’ll see the following output:

      Output

      blocked_hosts

      The pfctl utility can also parse the specific rules of your anchor with the -a and -s flags:

      • sudo pfctl -a blocked_hosts -s rules

      You will see the following output:

      Output

      block return out quick on egress from <blocked-hosts> to any

      Another feature of anchors is that they allow you to add rules on-demand without having to reload the ruleset. This can be useful for testing, quick-fixes, emergencies, and so on. For example if an internal host is acting peculiar and you want to block it from making outward connections, you can have an anchor in place that allows you to intervene quickly from the command line.

      Let’s open /etc/pf.conf and add another anchor:

      You’ll name the anchor rogue_hosts, and place it in the block all rule:

      /etc/pf.conf

      . . .
      block all
      anchor rogue_hosts
      . . .
      

      Save and exit the file.

      To initiate these changes, reload the ruleset with pfctl:

      • sudo pfctl -f /etc/pf.conf

      Once again, use pfctl to verify that the anchor is running:

      This will generate the following output:

      Output

      blocked_hosts rogue_hosts

      Now that the anchor is running, you can add rules to it at anytime. Test this by adding the following rule:

      • sudo sh -c 'echo "block return out quick on egress from 192.168.47.4" | pfctl -a rogue_hosts -f -'

      This invokes the echo command and its string content, which is then piped into the pfctl utility with the | symbol, where it is processed into an anchor rule. You open another shell session with the sh -c command. This is because you establish a pipe between two processes, but need sudo privileges to persist throughout the entire command sequence. There are multiple ways of resolving this; here you open an additional shell process with sudo privileges using sudo sh -c.

      Now, use pfctl again to verify that these rules are active:

      • sudo pfctl -a rogue_hosts -s rules

      This will generate the following output:

      Output

      block return out quick on egress inet from 192.168.47.4 to any

      The use of anchors is completely situational and often subjective. Like any other feature there are pros and cons to using anchors. Some applications such as blacklistd interface with anchors by design. Next you’ll focus on logging with PF, which is a critical aspect of network security. Your firewall is not useful if you can’t see what it is doing.

      Step 7 — Logging Your Firewall’s Activity

      In this step you’ll work with PF logging, which is managed by a pseudo-interface named pflog. Logging is enabled at boot-time by adding pflog_enabled=YES to the /etc/rc.conf file, which you did in Step 2. This enables the pflogd daemon that brings up an interface named pflog0 and writes logs in binary format to a file named /var/log/pflog. Logs can be parsed in realtime from the interface, or read from the /var/log/pflog file with the tcpdump(8) utility.

      First access some logs from the /var/log/pflog file:

      • sudo tcpdump -ner /var/log/pflog

      You pass the -ner flags that format the output for readability, and also specify a file to read from, which in your case is /var/log/pflog.

      You will see the following output:

      Output

      reading from file /var/log/pflog, link-type PFLOG (OpenBSD pflog file)

      In these early stages there may not be any data in the /var/log/pflog file. In a short period of time the log file will begin to grow.

      You can also view logs in realtime from the pflog0 interface by using the following command:

      You pass the -nei flags, which also format the output for readability, but this time specify an interface, which in your case is pflog0.

      You will see the following output:

      Output

      tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on pflog0, link-type PFLOG (OpenBSD pflog file), capture size 262144 bytes

      You will now see connections in realtime. If possible, ping your server from a remote machine and you will see the connections occurring. The server will remain in this state until you exit out of it.

      To exit out of this state and return to the command line hit CTRL + Z.

      There is a wealth of information on the internet about tcpdump(8), including the official website.

      Accessing Log Files with pftop

      The pftop utility is a tool for quickly viewing firewall activity in realtime. Its name is influenced by the well-known Unix top utility.

      To use it, you need to install the pftop package:

      Now run the pftop binary:

      This will generate the following output (your IPs will differ):

      Output

      PR DIR SRC DEST STATE AGE EXP PKTS BYTES tcp In 251.155.237.90:27537 157.225.173.58:22 ESTABLISHED:ESTABLISHED 00:12:35 23:59:55 1890 265K tcp In 222.186.42.15:25884 157.225.173.58:22 TIME_WAIT:TIME_WAIT 00:01:25 00:00:06 22 3801 udp Out 157.245.171.59:4699 67.203.62.5:53 MULTIPLE:SINGLE 00:00:14 00:00:16 2 227

      Creating Additional Log Interfaces

      Like any other interface, multiple log interfaces can be created and named with a /etc/hostname file. You may find this useful for organizational purposes, for example if you want to log certain types of activity separately.

      Create an additional logging interface named pflog1:

      • sudo vi /etc/hostname.pflog1

      Add the following contents to the /etc/hostname.pflog1 file:

      /etc/hostname.pflog1

      up
      

      Now enable the device at boot-time in your /etc/rc.conf file:

      • sudo sysrc pflog1_enable="YES"

      You can now monitor and log your firewall activity. This allows you to see who is making connections to your server and the types of connections being made.

      Throughout this tutorial you’ve incorporated some advanced concepts into your PF ruleset. It’s only necessary to implement advanced features as you need them. That said, in the next step you’ll revert back to the base ruleset.

      Step 8 — Reverting Back to Your Base Ruleset (Optional)

      In this final section you’ll revert back to your base ruleset. This is a quick step that will bring you back to a minimalist state of functionality.

      Open the base ruleset with the following command:

      Delete the current ruleset in your file and replace it with the following base ruleset:

      /etc/pf.conf

      vtnet0 = "vtnet0"
      icmp_types = "{ echoreq unreach }"
      table <bruteforce> persist
      table <rfc6890> { 0.0.0.0/8 10.0.0.0/8 100.64.0.0/10 127.0.0.0/8 169.254.0.0/16          
                        172.16.0.0/12 192.0.0.0/24 192.0.0.0/29 192.0.2.0/24 192.88.99.0/24    
                        192.168.0.0/16 198.18.0.0/15 198.51.100.0/24 203.0.113.0/24            
                        240.0.0.0/4 255.255.255.255/32 }
      
      set skip on lo0
      scrub in all fragment reassemble max-mss 1440
      antispoof quick for $vtnet0
      block in quick on $vtnet0 from <rfc6890>
      block return out quick on egress to <rfc6890>
      block all
      pass in on $vtnet0 proto tcp to port { 22 } 
          keep state (max-src-conn 15, max-src-conn-rate 3/1, 
              overload <bruteforce> flush global)
      pass out proto { tcp udp } to port { 22 53 80 123 443 }
      pass inet proto icmp icmp-type $icmp_types
      

      Save and exit the file.

      Reload the ruleset:

      • sudo pfctl -f /etc/pf.conf

      If there are no errors from the command, then there are no errors in your ruleset and your firewall is functioning properly.

      You also need to disable the pflog1 interface that you created. Since you might not know if you need it yet, you can disable pflog1 with the sysrc utility:

      • sudo sysrc pflog1_enable="NO"

      Now remove the /etc/hostname.pflog1 file from the /etc directory:

      • sudo rm /etc/hostname.pflog1

      Before signing off, reboot the server once more to ensure that all of your changes are active and persistent:

      Wait a few minutes before logging in to your server.

      Optionally, if you would like to implement PF with a webserver, the following is a ruleset for this scenario. This ruleset is a sufficient starting point for most web applications.

      Simple Web Server Ruleset

      vtnet0 = "vtnet0"
      icmp_types = "{ echoreq unreach }"
      table <bruteforce> persist
      table <webcrawlers> persist
      table <rfc6890> { 0.0.0.0/8 10.0.0.0/8 100.64.0.0/10 127.0.0.0/8 169.254.0.0/16          
                        172.16.0.0/12 192.0.0.0/24 192.0.0.0/29 192.0.2.0/24 192.88.99.0/24    
                        192.168.0.0/16 198.18.0.0/15 198.51.100.0/24 203.0.113.0/24            
                        240.0.0.0/4 255.255.255.255/32 }
      
      set skip on lo0
      scrub in all fragment reassemble max-mss 1440
      antispoof quick for $vtnet0
      block in quick on $vtnet0 from <rfc6890>
      block return out quick on egress to <rfc6890>
      block all
      pass in on $vtnet0 proto tcp to port { 22 } 
          keep state (max-src-conn 15, max-src-conn-rate 3/1, 
              overload <bruteforce> flush global)
      pass in on $vtnet0 proto tcp to port { 80 443 } 
          keep state (max-src-conn 45, max-src-conn-rate 9/1, 
              overload <webcrawlers> flush global)
      pass out proto { tcp udp } to port { 22 53 80 123 443 }
      pass inet proto icmp icmp-type $icmp_types
      

      This creates an overload table named <webcrawlers>, which has a more liberal overload policy than your SSH port based on the values of max-src-conn 45 and max-src-conn-rate. This is because not all overloads are from bad actors. They also can originate from non-malicious netbots, so you avoid excessive security measures on ports 80 and 443. If you decide to implement the webserver ruleset, you need to add the <webcrawlers> table to /etc/pf.conf, and clear the IPs from the table periodically. Refer to Step 5 for this.

      Conclusion

      In this tutorial, you configured PF on FreeBSD 12.1. You now have a base ruleset that can serve as a starting point for all of your FreeBSD projects. For further information on PF take a look at the pf.conf(5) man pages.

      Visit our FreeBSD topic page for more tutorials and Q&A.



      Source link

      How To Install Node.js on CentOS 8


      Introduction

      Node.js is a JavaScript runtime for server-side programming. It allows developers to create scalable backend functionality using JavaScript, a language many are already familiar with from browser-based web development.

      In this guide, we will show you three different ways of getting Node.js installed on a CentOS 8 server:

      • using dnf to install the nodejs package from CentOS’s default AppStream repository
      • installing nvm, the Node Version Manager, and using it to install and manage multiple versions of node
      • building and installing node from source

      Most users should use dnf to install the built-in pre-packaged versions of Node. If you’re a developer or otherwise need to manage multiple installed versions of Node, use the nvm method. Building from source is rarely necessary for most users.

      Prerequisites

      To complete this tutorial, you will need a server running CentOS 8. We will assume you are logged into this server as a non-root, sudo-enabled user. To set this up, see our Initial Server Setup for CentOS 8 guide.

      Option 1 — Installing Node from the CentOS AppStream Repository

      Node.js is available from CentOS 8’s default AppStream software repository. There are multiple versions available, and you can choose between them by enabling the appropriate module stream. First list out the available streams for the nodejs module using the dnf command:

      • sudo dnf module list nodejs

      Output

      Name Stream Profiles Summary nodejs 10 [d] common [d], development, minimal, s2i Javascript runtime nodejs 12 common, development, minimal, s2i Javascript runtime

      Two streams are available, 10 and 12. The [d] indicates that version 10 is the default stream. If you’d prefer to install Node.js 12, switch module streams now:

      • sudo dnf module enable nodejs:12

      You will be prompted to confirm your decision. Afterwards the version 12 stream will be enabled and we can continue with the installation. For more information on working with module streams, see the official CentOS AppStream documentation.

      Install the nodejs package with dnf:

      Again, dnf will ask you to confirm the actions it will take. Press y then ENTER to do so, and the software will install.

      Check that the install was successful by querying node for its version number:

      Output

      v12.13.1

      Your --version output will be different if you installed Node.js 10 instead.

      Note: both available versions of Node.js are long-term support releases, meaning they have a longer guaranteed window of maintenance. See the official Node.js releases page for more lifecycle information.

      Installing the nodejs package should also install the npm Node Package Manager utility as a dependency. Verify that it was installed properly as well:

      Output

      6.12.1

      At this point you have successfully instlled Node.js and npm using the CentOS software repositories. The next section will show how to use the Node Version Manager to do so.

      Option 2 — Installing Node Using the Node Version Manager

      Another way of installing Node.js that is particularly flexible is to use nvm, the Node Version Manager. This piece of software allows you to install and maintain many different independent versions of Node.js, and their associated Node packages, at the same time.

      To install NVM on your CentOS 8 machine, visit the project’s GitHub page. Copy the curl command from the README file that displays on the main page. This will get you the most recent version of the installation script.

      Before piping the command through to bash, it is always a good idea to audit the script to make sure it isn’t doing anything you don’t agree with. You can do that by removing the | bash segment at the end of the curl command:

      • curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh

      Take a look and make sure you are comfortable with the changes it is making. When you are satisfied, run the command again with | bash appended at the end. The URL you use will change depending on the latest version of NVM, but as of right now, the script can be downloaded and executed by typing:

      • curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash

      This will install the nvm script to your user account. To use it, you must first source your .bash_profile file:

      Now, you can ask NVM which versions of Node are available:

      nvm list-remote
      
      . . .
             v12.13.0   (LTS: Erbium)
             v12.13.1   (LTS: Erbium)
             v12.14.0   (LTS: Erbium)
             v12.14.1   (LTS: Erbium)
             v12.15.0   (LTS: Erbium)
             v12.16.0   (LTS: Erbium)
             v12.16.1   (Latest LTS: Erbium)
              v13.0.0
              v13.0.1
              v13.1.0
              v13.2.0
              v13.3.0
              v13.4.0
              v13.5.0
              v13.6.0
              v13.7.0
              v13.8.0
              v13.9.0
             v13.10.0
             v13.10.1
             v13.11.0
             v13.12.0
      

      It’s a very long list! You can install a version of Node by typing any of the release versions you see. For instance, to get version v13.6.0, you can type:

      You can see the different versions you have installed by typing:

      nvm list
      

      Output

      -> v13.6.0 default -> v13.6.0 node -> stable (-> v13.6.0) (default) stable -> 13.6 (-> v13.6.0) (default)

      This shows the currently active version on the first line (-> v13.6.0), followed by some named aliases and the versions that those aliases point to.

      Note: if you also have a version of Node installed through the CentOS software repositories, you may see a system -> v12.13.1 (or some other version number) line here. You can always activate the system version of Node using nvm use system.

      Additionally, you’ll see aliases for the various long-term support (or LTS) releases of Node:

      Output

      lts/* -> lts/erbium (-> N/A) lts/argon -> v4.9.1 (-> N/A) lts/boron -> v6.17.1 (-> N/A) lts/carbon -> v8.17.0 (-> N/A) lts/dubnium -> v10.19.0 (-> N/A) lts/erbium -> v12.16.1 (-> N/A)

      We can install a release based on these aliases as well. For instance, to install the latest long-term support version, erbium, run the following:

      Output

      Downloading and installing node v12.16.1... . . . Now using node v12.16.1 (npm v6.13.4)

      You can switch between installed versions with nvm use:

      nvm use v13.6.0
      
      Now using node v13.6.0 (npm v6.13.4)
      

      You can verify that the install was successful using the same technique from the other sections, by typing:

      node --version
      

      Output

      v13.6.0

      The correct version of Node is installed on our machine as we expected. A compatible version of npm is also available.

      Option 3 — Installing Node from Source

      Another way to install Node.js is to download the source code and compile it yourself.

      To do so, use your web browser to navigate to the official Node.js download page, right-click on the Source Code link and click Copy Link Address or whichever similar option your browser gives you.

      Back in your SSH session, first make sure you’re in a directory you can write to. We’ll use the current user’s home directory:

      Then type curl, paste the link that you copied from the website, and follow it with | tar xz:

      • curl https://nodejs.org/dist/v12.16.1/node-v12.16.1.tar.gz | tar xz

      This will use the curl utility to download the source, then pipe it directly to the tar utility, which will extract it into the current directory.

      Move into the newly created source directory:

      There are a few packages that we need to download from the CentOS repositories in order to compile the code. Use dnf to install these now:

      • sudo dnf install gcc-c++ make python2

      You will be prompted to confirm the installation. Type y then ENTER to do so. Now, we can configure and compile the software:

      The compilation will take quite a while (around 30 minutes on a four-core server). We’ve used the -j4 option to run four parallel compilation processes. You can omit this option or update the number based on the number of processor cores you have available.

      When compilation is finished, you can install the software onto your system by typing:

      To check that the installation was successful, ask Node to display its version number:

      v12.16.1
      

      If you see the correct version number, then the installation was completed successfully. By default Node also installs a compatible version of npm, so that should be available as well.

      Conclusion

      In this tutorial we’ve shown how to install Node.js using the CentOS AppStream software repository, using Node Version Manager, and by compiling from source.

      If you’d like more information on programming in JavaScript, please read our related tutorial series:



      Source link

      Understanding Default Parameters in JavaScript


      The author selected the COVID-19 Relief Fund to receive a donation as part of the Write for DOnations program.

      In ECMAScript 2015, default function parameters were introduced to the JavaScript language. These allow developers to initialize a function with default values if the arguments are not supplied to the function call. Initializing function parameters in this way will make your functions easier to read and less error-prone, and will provide default behavior for your functions. This will help you avoid errors that stem from passing in undefined arguments and destructuring objects that don’t exist.

      In this article, you will review the difference between parameters and arguments, learn how to use default parameters in functions, see alternate ways to support default parameters, and learn what types of values and expressions can be used as default parameters. You will also run through examples that demonstrate how default parameters work in JavaScript.

      Arguments and Parameters

      Before explaining default function parameters, it is important to know what it is that parameters can default to. Because of this, we will first review the difference between arguments and parameters in a function. If you would like to learn more about this distinction, check out our earlier article in the JavaScript series, How to Define Functions in JavaScript.

      In the following code block, you will create a function that returns the cube of a given number, defined as x:

      // Define a function to cube a number
      function cube(x) {
        return x * x * x
      }
      

      The x variable in this example is a parameter—a named variable passed into a function. A parameter must always be contained in a variable and must never have a direct value.

      Now take a look at this next code block, which calls the cube function you just created:

      // Invoke cube function
      cube(10)
      

      This will give the following output:

      Output

      1000

      In this case, 10 is an argument—a value passed to a function when it is invoked. Often the value will be contained in a variable as well, such as in this next example:

      // Assign a number to a variable
      const number = 10
      
      // Invoke cube function
      cube(number)
      

      This will yield the same result:

      Output

      1000

      If you do not pass an argument to a function that expects one, the function will implicitly use undefined as the value:

      // Invoke the cube function without passing an argument
      cube()
      

      This will return:

      Output

      NaN

      In this case, cube() is trying to calculate the value of undefined * undefined * undefined, which results in NaN, or “not a number”. For more on this, take a look at the number section of Understanding Data Types in JavaScript.

      This automatic behavior can sometimes be a problem. In some cases, you might want the parameter to have a value even if no argument was passed to the function. That’s where the default parameters feature comes in handy, a topic that you will cover in the next section.

      Default Parameter Syntax

      With the addition of default parameters in ES2015, you can now assign a default value to any parameter, which the function will use instead of undefined when called without an argument. This section will first show you how to do this manually, and then will guide you through setting default parameters.

      Without default parameters, you would have to explicitly check for undefined values in order to set defaults, as is shown in this example:

      // Check for undefined manually
      function cube(x) {
        if (typeof x === 'undefined') {
          x = 5
        }
      
        return x * x * x
      }
      
      cube()
      

      This uses a conditional statement to check if the value has been automatically provided as undefined, then sets the value of x as 5. This will result in the following output:

      Output

      125

      In contrast, using default parameters accomplishes the same goal in much less code. You can set a default value to the parameter in cube by assigning it with the equality assignment operator (=), as highlighted here:

      // Define a cube function with a default value
      function cube(x = 5) {
        return x * x * x
      }
      

      Now when the cube function is invoked without an argument, it will assign 5 to x and return the calculation instead of NaN:

      // Invoke cube function without an argument
      cube()
      

      Output

      125

      It will still function as intended when an argument is passed, ignoring the default value:

      // Invoke cube function with an argument
      cube(2)
      

      Output

      8

      However, one important caveat to note is that the default parameter value will also override an explicit undefined passed as an argument to a function, as demonstrated here:

      // Invoke cube function with undefined
      cube(undefined)
      

      This will give the calculation with x equal to 5:

      Output

      125

      In this case, the default parameter values were calculated, and an explicit undefined value did not override them.

      Now that you have an idea of the basic syntax of default parameters, the next section will show how default parameters work with different data types.

      Default Parameter Data Types

      Any primitive value or object can be used as a default parameter value. In this section, you will see how this flexibility increases the ways in which default parameters can be used.

      First, set parameters using a number, string, boolean, object, array, and null value as a default value. This example will use arrow function syntax:

      // Create functions with a default value for each data type
      const defaultNumber = (number = 42) => console.log(number)
      const defaultString = (string = 'Shark') => console.log(string)
      const defaultBoolean = (boolean = true) => console.log(boolean)
      const defaultObject = (object = { id: 7 }) => console.log(object)
      const defaultArray = (array = [1, 2, 3]) => console.log(array)
      const defaultNull = (nullValue = null) => console.log(nullValue)
      

      When these functions are invoked without parameters, they will all use the default values:

      // Invoke each function
      defaultNumber()
      defaultString()
      defaultBoolean()
      defaultObject()
      defaultArray()
      defaultNull()
      

      Output

      42 "Shark" true {id: 7} (3) [1, 2, 3] null

      Note that any object created in a default parameter will be created every time the function is called. One of the common use cases for default parameters is to use this behavior to obtain values out of an object. If you try to destructure or access a value from an object that doesn’t exist, it will throw an error. However, if the default parameter is an empty object, it will simply give you undefined values instead of throwing an error:

      // Define a settings function with a default object
      function settings(options = {}) {
        const { theme, debug } = options
      
        // Do something with settings
      }
      

      This will avoid the error caused by destructuring objects that don’t exist.

      Now that you’ve seen how default parameters operate with different data types, the next section will explain how multiple default parameters can work together.

      Using Multiple Default Parameters

      You can use as many default parameters as you want in a function. This section will show you how to do this, and how to use it to manipulate the DOM in a real-world example.

      First, declare a sum() function with multiple default parameters:

      // Define a function to add two values
      function sum(a = 1, b = 2) {
        return a + b
      }
      
      sum()
      

      This will result in the following default calculation:

      Output

      3

      Additionally, the value used in a parameter can be used in any subsequent default parameter, from left to right. For example, this createUser function creates a user object userObj as the third parameter, and all the function itself does is return userObj with the first two parameters:

      // Define a function to create a user object using parameters
      function createUser(name, rank, userObj = { name, rank }) {
        return userObj
      }
      
      // Create user
      const user = createUser('Jean-Luc Picard', 'Captain')
      

      If you call user here, you will get the following:

      Output

      {name: "Jean-Luc Picard", rank: "Captain"}

      It is usually recommended to put all default parameters at the end of a list of parameters, so that you can easily leave off optional values. If you use a default parameter first, you will have to explicitly pass undefined to use the default value.

      Here is an example with the default parameter at the beginning of the list:

      // Define a function with a default parameter at the start of the list
      function defaultFirst(a = 1, b) {
        return a + b
      }
      

      When calling this function, you would have to call defaultFirst() with two arguments:

      defaultFirst(undefined, 2)
      

      This would give the following:

      Output

      3

      Here is an example with the default parameter at the end of the list:

      // Define a function with a default parameter at the end of the list
      function defaultLast(a, b = 1) {
        return a + b
      }
      
      defaultLast(2)
      

      This would yield the same value:

      Output

      3

      Both functions have the same result, but the one with the default value last allows a much cleaner function call.

      For a real-world example, here is a function that will create a DOM element, and add a text label and classes, if they exist.

      // Define function to create an element
      function createNewElement(tag, text, classNames = []) {
        const el = document.createElement(tag)
        el.textContent = text
      
        classNames.forEach(className => {
          el.classList.add(className)
        })
      
        return el
      }
      

      You can call the function with some classes in an array:

      const greeting = createNewElement('p', 'Hello!', ['greeting', 'active'])
      

      Calling greeting will give the following value:

      Output

      <p class="greeting active">Hello!</p>

      However, if you leave the classNames array out of the function call, it will still work.

      const greeting2 = createNewElement('p', 'Hello!')
      

      greeting2 now has the following value:

      Output

      <p>Hello!</p>

      In this example, forEach() can be used on an empty array without an issue. If that empty array were not set in the default parameter, you would get the following error:

      Output

      VM2673:5 Uncaught TypeError: Cannot read property 'forEach' of undefined at createNewElement (<anonymous>:5:14) at <anonymous>:12:18

      Now that you have seen how multiple default parameters can interact, you can move on to the next section to see how function calls work as default parameters.

      Function Calls as Default Parameters

      In addition to primitives and objects, the result of calling a function can be used as a default parameter.

      In this code block, you will create a function to return a random number, and then use the result as the default parameter value in a cube function:

      // Define a function to return a random number from 1 to 10
      function getRandomNumber() {
        return Math.floor(Math.random() * 10)
      }
      
      // Use the random number function as a default parameter for the cube function
      function cube(x = getRandomNumber()) {
        return x * x * x
      }
      

      Now invoking the cube function without a parameter will have potentially different results every time you call it:

      // Invoke cube function twice for two potentially different results
      cube()
      cube()
      

      The output from these function calls will vary:

      Output

      512 64

      You can even use built-in methods, like those on the Math object, and use the value returned in one function call as a parameter in another function.

      In the following example, a random number is assigned to x, which is used as the parameter in the cube function you created. The y parameter will then calculate the cube root of the number and check to see if x and y are equal:

      // Assign a random number to x
      // Assign the cube root of the result of the cube function and x to y
      function doesXEqualY(x = getRandomNumber(), y = Math.cbrt(cube(x))) {
        return x === y
      }
      
      doesXEqualY()
      

      This will give the following:

      Output

      true

      A default parameter can even be a function definition, as seen in this example, which defines a parameter as the inner function and returns the function call of parameter:

      // Define a function with a default parameter that is an anonymous function
      function outer(
        parameter = function inner() {
          return 100
        }
      ) {
        return parameter()
      }
      
      // Invoke outer function
      outer()
      

      Output

      100

      This inner function will be created from scratch every time the outer function is invoked.

      Conclusion

      In this article, you learned what default function parameters are and how to use them. Now you can use default parameters to help keep your functions clean and easy to read. You can also assign empty objects and arrays to parameters upfront to reduce both complexity and lines of code when dealing with situations such as retrieving values from an object or looping through an array.

      If you would like to learn more about JavaScript, check out the homepage for our How To Code in JavaScript series, or browse our How to Code in Node.js series for articles on back-end development.



      Source link