How a typo broke TCP scan in the UMS Web App

A story of how a simple typo broke an important feature in the IGEL UMS Web App... and how to fix it :)

Intro image to "How a typo broke TCP scan in the UMS Web App"

This started out looking like a simple UI parity problem.

In the IGEL UMS Console, device discovery supports TCP scanning on both the local network and an IP range. That lets administrators discover and onboard IGEL OS devices over TCP on port 30005, rather than using the default UDP scan, running on port 30005.

In some environments, the fewer ports open, the better. In others, TCP is a must if it’s available.

In the UMS Web App, things look slightly different:

Interface Scan type TCP option
Classic UMS Console Local network Available
Classic UMS Console IP range Available
UMS Web App Local network Available
UMS Web App IP range Not exposed

I assumed TCP scanning over an IP range simply hadn’t made it into the Web App yet. Some features were still being ported from the UMS Console, and the missing option made that seem reasonable.

Turns out, that assumption was wrong.

I’d seen the same behavior on earlier versions - After updating to the latest UMS release available at the time (12.14.100) and finding the issue was still there, I decided to take a closer look.


The missing checkbox

According to the IGEL documentation for the Scan and Register function in the UMS Web App, TCP scanning should already be available there.

When I selected Local network of the UMS Server, the option was clearly there.

Use TCP for scanning being available

But when I switched to IP Range, it was gone.

Selecting IP Range, “Use TCP for scanning” is missing


Looking behind the scenes

When I ran a scan, the UMS Web App sent a device-discovery request to:

The search sends a request to sub-url "scanfordevices"
POST /wums-app/device-command/scanfordevices

For an IP-range scan, the request looked like this:

The JSON inside the request payload

{
  "fromip": "10.27.4.20",
  "toip": "10.27.4.200",
  "useTcpScan": true
}

Well, that was unexpected.

There was no TCP checkbox in the UI for IP-range mode, but the frontend was still sending:

"useTcpScan": true

The compiled Web App JavaScript explained why: for an IP-range scan, the request builder hard-coded:

useTcpScan: true

So the missing checkbox didn’t mean IP-range scanning was supposed to use UDP. Quite the opposite. The frontend was asking for TCP.

Or at least, it thought it was.


The firewall disagreed

The IP of the UMS server in question was:

10.80.200.107

With the target range being:

10.27.4.20 - 10.27.4.200

Despite the browser sending useTcpScan: true, the firewall showed:

Firewall logs showing UDP was used

UDP/30005

For example:

10.80.200.107 -> 10.27.4.253 UDP/30005
10.80.200.107 -> 10.27.4.250 UDP/30005
10.80.200.107 -> 10.27.4.247 UDP/30005
10.80.200.107 -> 10.27.4.244 UDP/30005

This wasn’t just cosmetic. The request appeared to ask for TCP, but the network showed UDP.

I also tried sending the value as a string instead of a JSON Boolean:

"useTcpScan": "true"

Changing the value of the payload

Firewall logs still showing UDP in use

Still UDP.

I started to wonder whether the Web App and the classic Console were sending different parameter names.

That turned out to be the key.


Following the request through the Web App

Tracing scanfordevices through the Web App showed that the endpoint accepts the browser payload as a generic parameter map.

For example:

@PostMapping(value={"/scanfordevices"})
ResponseEntity<List<ScanDeviceResult>> scanForDevices(
    @RequestBody Map<String, String> cmdParams
) {
    return ResponseEntity.ok(
        this.deviceCommandDispatcher.scanForDevices(cmdParams)
    );
}

The Web App then passes the same map through its service chain until it reaches:

apitccmds/tccommands/scanfordevices

The key point wasn’t each individual step. I couldn’t find any code that translated useTcpScan before the request reached the existing scan endpoint.

The request travels roughly like this:

Browser
 |
 | JSON parameter map
 v
/wums-app/device-command/scanfordevices
 |
 | forwarded unchanged
 v
Web App service chain
 |
 v
apitccmds/tccommands/scanfordevices

That left one obvious question:
What does the working classic Console send?


Looking at the classic Console

The classic Console already supports Use TCP for scanning during an IP-range scan, so obviously I wanted to see exactly what it sent.

Classic UMS Console showing the TCP scan option

The key detail was how it built the scan parameters:

cmdParams.put(
    "usetcpscan",
    String.valueOf(tcpScan)
);

The command constants confirmed the same parameter name:

public static final String PARAM_USETCPSCAN = "usetcpscan";

There it was.

The Web App sends:

useTcpScan

The classic Console sends:

usetcpscan

Visually, the difference is almost ridiculous:

Classic Console
---------------
usetcpscan=true
 |
 v
TCP scan works

Web App
---------------
useTcpScan=true
 |
 v
Parameter ignored
 |
 v
UDP scan

I still wanted to rule out one detail: the classic Console sends the value as a string, while the Web App sends a JSON Boolean. So I tested both.


A typo. Or a type?

Here’s the test matrix:

Test Payload Result
Original Web App "useTcpScan": true UDP
Same key, string value "useTcpScan": "true" UDP
Correct key, Boolean value "usetcpscan": true TCP
Correct key, string value "usetcpscan": "true" TCP

Changing the value to a string still produced UDP:

Changing the value of the payload

Firewall logs still showing UDP in use

With the corrected key, the result changed to TCP:

Firewall logs showing TCP in use

Using a JSON Boolean with the corrected key also produced TCP:

Firewall logs still showing TCP

That ruled out the value type. The bug was the parameter name:

- useTcpScan
+ usetcpscan

That was it.


The existing TCP checkbox was broken too

I still hadn’t tested the existing Use TCP for scanning option. It scans the UMS Server’s local subnet, so I took a quick look at that request too.

Use TCP for scanning enabled for a local network scan

It used the same parameter:

Local Network "TCP" scan
{
  "useTcpScan": true
}

The firewall still showed UDP/30005:

Firewall logs showing the broken TCP option for a local network scan

So the problem wasn’t limited to the hidden IP-range option. The visible Use TCP for scanning checkbox for a local-network scan sent the same incorrectly named parameter.

I couldn’t repeat the corrected-key replay for this request. Attempts to edit and resend it resulted in either HTTP 403 or NS_ERROR_NET_RESET.

That didn’t change the IP-range result, but it meant I couldn’t independently confirm the corrected local-network request using the same browser replay method.


So what actually broke?

The Web App frontend sends:

{
    useTcpScan: true,
    fromip: "...",
    toip: "..."
}

The existing UMS scan path expects:

{
    usetcpscan: true,
    fromip: "...",
    toip: "..."
}

The Web App forwards the parameter without translating its name, so the scan path doesn’t recognize the TCP flag. The scan therefore uses UDP instead.

Web App
    |
    | useTcpScan: true
    v
Web App backend
    |
    | forwarded unchanged
    v
UMS scan path
    |
    | expects usetcpscan
    v
TCP flag ignored
    |
    v
UDP/30005

With the corrected key, the scan path recognizes the parameter:

Web App
    |
    | usetcpscan: true
    v
UMS scan path
    |
    | parameter recognised
    v
TCP/30005

The capability wasn’t missing. The Web App was just asking for it under the wrong name.


Now how do we fix it?

Finding the typo was one thing. I also wanted to see whether correcting it was enough, or whether something else in the Web App would get in the way.

Since this was a lab UMS, I made a temporary change to the active frontend bundle.

The interesting part was that I didn’t need to build a new TCP option.

It was already there.

The scan dialog already had the Use TCP for scanning checkbox, along with its form control, translation string and state handling. The Web App simply hid the checkbox whenever IP Range was selected.

In the compiled template, that behavior boiled down to:

D(t.showIpRange()?12:-1),a(),D(t.showIpRange()?-1:13)

Slot 12 is the IP-range field.

Slot 13 is the existing TCP checkbox.

For the lab test, I changed it to:

D(t.showIpRange()?12:-1),a(),D(13)

After a hard refresh, there it was:

Showcase of the now working TCP button

The request builder still needed the parameter name fixed. It contained:

e=Be({useTcpScan:!0},this.getIpRangeValues())

For the test, I changed it to:

e=Be({usetcpscan:this.dialogForm.controls.useTcpScan.value},this.getIpRangeValues())

The Web App now followed the checkbox.

Unchecked:

{
  "usetcpscan": false,
  "fromip": "10.80.105.0",
  "toip": "10.80.105.2"
}

produced UDP/30005:

Firewall logs of unchecked TCP option

Checked:

{
  "usetcpscan": true,
  "fromip": "10.80.105.0",
  "toip": "10.80.105.2"
}

produced TCP SYNs:

10.80.200.107 -> 10.80.105.0:30005  SYN
10.80.200.107 -> 10.80.105.1:30005  SYN
10.80.200.107 -> 10.80.105.2:30005  SYN

TCP option enabled for the IP-range scan

Firewall logs showing working TCP option

That closed the loop.

The checkbox already existed.
The TCP scan implementation already existed.
The IP-range scan implementation already existed.

For the IP-range path, the lab proof of concept only needed the existing checkbox exposed and:

- useTcpScan
+ usetcpscan

I wouldn’t run a modified UMS frontend like this as a production fix. The file is part of the installed Web App, so an update or redeployment could replace it, and it isn’t an IGEL-supported modification.

As a proof of concept, though, it showed there was no missing TCP feature left to implement.

The pieces were already there. They just weren’t connected correctly.


IGEL’s response

After I shared my findings, reproduction steps and lab proof of concept with IGEL Support, they reviewed the material and reproduced the issue. Support confirmed that the behavior was caused by a bug and escalated the case to the Development Team:

“Thank you for providing the video and all the additional files and information. They were very helpful in reviewing and reproducing the issue.

As you have already proved the behavior you are experiencing is caused by a bug. The case will therefore be escalated to our Development Team.”

In my lab, the fix only required exposing the existing checkbox for IP Range and using the parameter name the scan endpoint expects.

Since the TCP scan support is already there, this looks like a relatively small fix to a feature that’s currently broken in the Web App. IGEL haven’t confirmed a fix or target version yet, but I suspect it’ll be included in an upcoming release :)


Conclusion

The surprising part wasn’t that TCP scanning was broken.

It was that it had never really been missing.

The UI control existed.
The backend support existed.
The scan implementation existed.

A single parameter-name mismatch:

- useTcpScan
+ usetcpscan

was enough for the scan to ignore the TCP flag and use UDP instead.

Once the request used the expected key, the existing implementation worked over TCP/30005 without any backend changes.